diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/resources |
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/resources')
143 files changed, 12522 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.css b/www/wiki/extensions/Translate/resources/css/ext.translate.css new file mode 100644 index 00000000..9b4c65d6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.css @@ -0,0 +1,8 @@ +.mw-translate-fuzzy { + background-color: #fdd; +} + +.mw-pt-translate-header { + font-size: x-small; + text-align: center; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.dropdownmenu.css b/www/wiki/extensions/Translate/resources/css/ext.translate.dropdownmenu.css new file mode 100644 index 00000000..46d17ed8 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.dropdownmenu.css @@ -0,0 +1,13 @@ +.tux-dropdown-menu { + border: 1px solid #c9c9c9; + /* @noflip */ + box-shadow: 0 3px 3px -3px rgba( 0, 0, 0, 0.5 ); + font-size: 14px; + margin: 0; + list-style: none; + padding: 4px; + z-index: 300; + background: #fff; + display: block; + position: absolute; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.editor.css b/www/wiki/extensions/Translate/resources/css/ext.translate.editor.css new file mode 100644 index 00000000..f981bf44 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.editor.css @@ -0,0 +1,460 @@ +.tux-message-editor { + position: relative; + border: 1px solid #777; + background-color: #fff; + cursor: default; + box-shadow: 0 2px 6px rgba( 0, 0, 0, 0.3 ); +} + +.grid .tux-message-editor .close { + background: no-repeat center center; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/close.svg ); + padding: 16px; + cursor: pointer; + float: right; + opacity: 0.87; +} + +.grid .tux-message-editor .editor-info-toggle { + padding: 16px; + cursor: pointer; + float: right; +} + +.tux-message-editor .editor-contract { + background: no-repeat center center; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/contract-ltr.svg ); +} + +.tux-message-editor .editor-expand { + background: no-repeat center center; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/expand-ltr.svg ); +} + +.tux-message-editor .editcolumn { + border-right: 1px solid #aaa; + transition: width 0.5s; + background-color: #fff; + z-index: 1; + /* Padding to have space for the arrow indicating split section */ + padding-right: 40px; +} + +.grid .tux-message-editor--expanded .editcolumn { + width: 100%; + padding-right: 5px; +} + +.grid .tux-message-editor .tux-editor-titletools { + /* Ignore the padding added for the arrow */ + margin-right: -40px; +} + +.grid .tux-message-editor--expanded .tux-editor-titletools { + margin-right: -5px; +} + +.tux-message-editor textarea { + border: 1px solid #555; + font-size: 16px; + /* The (30px + 5px paddings) 40px for bottom is for the insertables */ + padding: 5px 5px 40px 5px; + /* Normalize margin across skins (esp. Timeless) */ + margin: 0; + height: 100px; + min-height: 150px; + overflow-y: auto; + position: relative; + z-index: 100; + /* We have automatic resizing for height, and horizontal makes no sense */ + resize: none; + /* Avoid weird extra space appearing at the bottom of enclosing div when + * the default value inline-block is used in Chrome. + * https://stackoverflow.com/questions/5196424/inconsistent-textarea-handling-in-browsers */ + display: block; +} + +.tux-editor-editsummary-block input { + border: 1px solid #c0c0c0; + font-size: 14px; + width: 100%; + height: 30px; + margin: 5px 0 0; + padding: 1px 4px; +} + +.tux-editor-editsummary-block input:disabled { + background-color: #f8f8f8; +} + +.tux-message-editor .editarea { + position: relative; +} + +/* Temporary fix for T111685 */ +.grid .tux-message-editor .messagekey { + color: #222; + font-size: 13px; + font-weight: bold; + padding: 5px 0 5px 10px; + cursor: pointer; +} + +.tux-message-editor .messagekey .caret { + border-top: 4px solid #222; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display: inline-block; + vertical-align: middle; + margin: 0 4px; +} + +/* Temporary fix for T111685 */ +.grid .tux-message-editor .sourcemessage { + font-size: 18px; + line-height: 1.6em; + padding: 5px 0 10px 10px; + word-wrap: break-word; +} + +.tux-message-editor .sourcemessage.long { + font-size: 16px; +} + +.tux-message-editor .sourcemessage.longer { + font-size: 14px; +} + +.tux-message-editor .shortcutinfo { + color: #54595d; + font-size: 13px; + padding: 0 5px 5px 10px; + display: none; +} + +@media screen and ( min-width: 980px ) { + .tux-message-editor .shortcutinfo { + display: block; + } +} + +.tux-message-editor .infocolumn-block .infocolumn { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 10px; + padding: 0 5px; + overflow: auto; +} + +/* Temporary fix for T111685 */ +.grid .tux-message-editor .infocolumn-block { + font-size: 12pt; + background: #fcfcfc; + position: absolute; + top: 0; + right: 0; + bottom: 0; + padding: 0; + transition: width 0.5s; +} + +.tux-message-editor__caret:before, +.tux-message-editor__caret:after { + border-top: 20px solid transparent; + border-right: 20px solid #aaa; + border-bottom: 20px solid transparent; + content: ''; + display: inline-block; + left: -21px; + position: absolute; + bottom: 50%; + z-index: 2; + transition: visibility 0s 0.5s; +} + +.tux-message-editor__caret:after { + border-right: 20px solid #fcfcfc; + left: -20px; +} + +.tux-message-editor--expanded .tux-message-editor__caret:before, +.tux-message-editor--expanded .tux-message-editor__caret:after { + visibility: hidden; + transition: visibility 0s 0s; +} + +.infocolumn-block .infocolumn .message-desc-editor { + padding: 5px 0; + margin-right: 5px; +} + +.tux-textarea-documentation { + height: 100px; + overflow: auto; +} + +.infocolumn-block .infocolumn .message-desc { + font-size: 16px; + padding: 5px 0; + margin-right: 5px; +} + +.infocolumn-block .infocolumn .message-desc.long { + font-size: 14px; + border-bottom: 1px solid #ddd; +} + +.infocolumn-block .infocolumn .message-desc.compact { + max-height: 100px; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-desc-control { + font-size: 14px; + padding: 3px 0 5px 0; +} + +.message-desc-control .read-more { + font-size: 14px; + color: #36c; + cursor: pointer; + margin-right: 5px; +} + +.message-desc-edit { + background: left center no-repeat; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/action-edit.svg ); + background-size: 18px 18px; + padding-left: 19px; +} + +.tm-suggestions-title, +.in-other-languages-title { + font-size: 16px; + font-weight: bold; + padding: 10px 0; +} + +.tm-suggestion, +.in-other-language { + font-size: 14px; + border: 1px solid #ddd; + border-left: 2px solid #36c; + padding: 5px 5px 5px 10px; + background-color: #f5f5f5; +} + +.grid .row .tm-suggestion, +.grid .row .in-other-language { + margin: 0 5px 3px -5px; +} + +.tm-suggestion:hover, +.in-other-language:hover { + cursor: pointer; + box-shadow: 0 0 3px rgba( 0, 0, 0, 0.2 ); +} + +.in-other-language .language { + color: #54595d; + overflow: hidden; + text-overflow: ellipsis; +} + +.tux-message-editor .help { + font-size: 16px; + padding: 10px 5px; +} + +.tux-message-editor .help a { + font-size: 14px; + padding: 0 0 0 5px; +} + +.editarea .tux-warnings-block { + position: relative; +} + +.tux-warning { + background-color: #ffc; +} + +.tux-highlight { + background-color: #c9c9c9; +} + +.tux-warning-message { + font-size: 14px; + padding: 2px 15% 2px 5px; + /* 15px space for icon */ + padding-left: 20px; + background-position: left; + background-repeat: no-repeat; +} + +.tux-warning .diff { + font-size: 12px; + padding: 0 0 0 20px; + /* 15px space for icon */ +} + +.editarea .tux-more-warnings { + background-color: #fbf6ad; + position: absolute; + right: 0; + padding: 2px 5px; + text-align: right; + cursor: pointer; + bottom: 0; +} + +.tux-warning-message.validation { + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/label-flag.svg ); +} + +.tux-warning-message.diff { + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/label-clock.svg ); +} + +.tux-warning-message .show-diff-link { + color: #36c; + cursor: pointer; + padding-left: 5px; +} + +.editarea .tux-more-warnings:before { + content: ''; + border-bottom: 1em solid #fbf6ad; + border-top: 1em solid transparent; + border-left: 1em solid transparent; + border-right: 1em solid #fbf6ad; + display: inline-block; + position: absolute; + right: 100%; + top: 0; +} + +.tux-editor-request-right { + font-size: 13px; + padding: 0 5px; + color: #54595d; +} + +.tux-editor-ask-permission { + padding: 0 5px; +} + +.tux-editor-editarea-block { + padding: 0 5px; +} + +.tux-editor-editsummary-block { + padding: 0 5px; +} + +.tux-editor-actions-block { + position: relative; +} + +/* Temporary fix for T111685 */ +.grid .tux-editor-actions-block .tux-editor-insert-buttons { + position: absolute; + /* 30px + 5px padding on bottom */ + top: -35px; + margin: 0 10px; + z-index: 110; +} + +.tux-editor-insert-buttons button { + padding: 0 5px; + min-width: 30px; + margin-right: 5px; + margin-bottom: 5px; + border: 1px solid #ddd; + background: #fbfbfb; + color: #222; + font-size: 13px; + line-height: 30px; + height: 30px; +} + +.tux-editor-insert-buttons .tux-editor-paste-original-button { + background: #fbfbfb left center no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/paste.svg ); + background-size: 16px 16px; + padding-left: 18px; +} + +.message-desc-editor .mw-ui-button, +.tux-editor-control-buttons .mw-ui-button { + margin: 10px 5px; +} + +.sourcemessage, +.suggestiontext { + white-space: pre-wrap; +} + +.infocolumn .loading { + color: #54595d; + padding: 10px; + font-size: 14px; +} + +.tux-message-tools-menu li a { + color: #54595d; + display: block; + font-size: 14px; + padding: 0 2px; + text-decoration: none; +} + +.tux-message-tools-menu li a:hover { + cursor: pointer; + background-color: #f0f0f0; + color: #222; +} + +.tux-message-tools-menu li.selected { + background: right no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/label-tick.svg ); + color: #222; +} + +/* + * Hide the cancel button by default, but show it in the last message. + * !important is used to override the button styling in ULS. +*/ +.tux-editor-cancel-button { + display: none !important; +} + +.last-message .tux-editor-cancel-button { + display: inline-block !important; +} + +.last-message .tux-editor-skip-button { + display: none; +} + +.shortcut-popup { + width: 18px; + height: 18px; + line-height: 18px; + overflow: hidden; + font-size: 13px; + text-align: center; + border: 1px dashed #808080; + border-radius: 100%; + z-index: 110; + background-color: #fff; + padding: 3px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.css b/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.css new file mode 100644 index 00000000..3b764a1a --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.css @@ -0,0 +1,167 @@ +/** + * Group selector + */ +.tux-groupselector { + position: absolute; + top: 14px; + right: 0; + z-index: 1000; + display: none; + float: left; + margin-top: 13px; + min-width: 600px; + width: 600px; + padding: 0; + border: 1px solid #888; + background-color: #F0F0F0; + border-radius: 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + text-align: left; +} + +/* The triangle shaped callout */ +.tux-groupselector:before { + border-bottom: 7px solid #888; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + content: ""; + display: inline-block; + left: 99px; + position: absolute; + top: -7px; +} + +.tux-groupselector:after { + border-bottom: 6px solid #F0F0F0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + content: ""; + display: inline-block; + left: 100px; + position: absolute; + top: -6px; +} + +/* Remove the triangle shaped callout */ +.tux-groupselector.removecallout:before, +.tux-groupselector.removecallout:after { + content: none; +} + +.grid .row .tux-groupselector__title { + border: none; + color: #555555; + font-size: 14pt; + font-weight: normal; + line-height: 1.25em; + padding: 5px 0 0 10px; /* grid override */ + margin: 0; +} + +.tux-groupselector__filter { + height: 30px; + border-bottom: solid 1px #c9c9c9; +} + +.tux-groupselector__filter__search__input { + font-size: 14px; + width: 100%; + height: 28px; + border: 1px solid #C9C9C9; +} + +.tux-groupselector__filter__search__icon { + background: url(../images/search.png) no-repeat scroll right center transparent; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/search.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/search.svg); + background-size: 25px; + height: 28px; + width: 28px; +} + +.tux-groupselector button { + height: 32px; + margin: 5px; + font-weight: bold; +} + +/* + * Group tab + */ +.tux-grouptab { + box-sizing: border-box; + line-height: 30px; + height: 30px; + color: #252525; + cursor: pointer; + padding: 2px 5px; + margin: 0 4px; + display: inline-block; +} + +.tux-grouptab--selected { + border-bottom: 2px solid #0645AD; +} + +/* + * Group list + */ +.tux-grouplist { + max-height: 400px; + overflow-y: auto; + overflow-x: hidden; + background-color: #FFFFFF; + min-height: 200px; + border-radius: 0 0 5px 5px; +} + +.tux-grouplist__item { + position: relative; + border-bottom: 1px solid #EEEEEE; + height: 50px; + cursor: pointer; +} + +.grid .tux-grouplist__item__label { + padding-bottom: 3px; /* grid override */ + padding-left: 15px; /* grid override */ + font-weight: normal; + line-height: 40px; + cursor: pointer; + white-space: nowrap; +} + +.tux-grouplist__item__label .tux-statsbar { + position: absolute; + bottom: 0; + width: 150px; +} + +.tux-grouplist__item__icon { + background: url(../images/project.png) no-repeat scroll right center transparent; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/project.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/project.svg); + /* Keep this in sync with js! */ + background-size: 32px; + height: 50px; + width: 50px; +} + +.grid .row .tux-grouplist__item__subgroups { + position: absolute; /* grid override */ + padding: 0 15px 2px 0; /* grid override */ + font-weight: normal; + line-height: 1.25em; + bottom: 0; + right: 0; + text-align: right; + color: #777; + font-size: 10pt; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.less b/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.less new file mode 100644 index 00000000..025dfa85 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.less @@ -0,0 +1,143 @@ +@import 'mediawiki.mixins'; + +/** + * Group selector + */ +.tux-groupselector { + position: absolute; + top: 14px; + right: 0; + z-index: 1000; + display: none; + margin-top: 13px; + width: 600px; + padding: 0; + border: 1px solid #a2a9b1; + background-color: #f0f0f0; + border-radius: 5px; + box-shadow: 0 5px 10px rgba( 0, 0, 0, 0.2 ); +} + +/* The triangle shaped callout */ +.tux-groupselector:before { + border-bottom: 7px solid #a2a9b1; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + content: ''; + display: inline-block; + left: 99px; + position: absolute; + top: -7px; +} + +.tux-groupselector:after { + border-bottom: 6px solid #f0f0f0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + content: ''; + display: inline-block; + left: 100px; + position: absolute; + top: -6px; +} + +/* Remove the triangle shaped callout */ +.tux-groupselector.removecallout:before, +.tux-groupselector.removecallout:after { + content: none; +} + +.tux-groupselector__filter { + padding-top: 10px; +} + +.tux-groupselector__filter__search__input { + font-size: 14px; + width: 100%; + height: 28px; + border: 1px solid #c9c9c9; + padding: 2px; + margin: 0; +} + +.tux-groupselector__filter__search__icon { + .background-image( '../images/search.svg' ); + background-repeat: no-repeat; + background-position: right center; + background-size: 25px; + height: 28px; +} + +/* + * Group tab + */ +.tux-grouptab { + color: #222; + line-height: 30px; + height: 30px; + cursor: pointer; + padding: 2px 5px; + margin: 0 4px; + display: inline-block; +} + +.tux-grouptab--selected { + border-bottom: 2px solid #0645ad; +} + +/* + * Group list + */ +.tux-grouplist { + max-height: 400px; + overflow-y: auto; + overflow-x: hidden; + background-color: #fff; + min-height: 200px; + border-radius: 0 0 5px 5px; +} + +.tux-grouplist__item { + position: relative; + border-bottom: 1px solid #eee; + height: 50px; + cursor: pointer; + + &:hover { + background-color: #f8f8f8; + } +} + +.grid .tux-grouplist__item__label { + padding-bottom: 0; /* grid override */ + padding-left: 15px; /* grid override */ + line-height: 32px; + cursor: pointer; + white-space: nowrap; +} + +.tux-grouplist__item__label .tux-statsbar { + position: absolute; + bottom: 0; + width: 150px; +} + +.tux-grouplist__item__icon { + .background-image( '../images/project.svg' ); + background-repeat: no-repeat; + background-position: right center; + /* Keep this in sync with js! */ + background-size: 32px; + height: 50px; +} + +.grid .row .tux-grouplist__item__subgroups { + color: #72777d; + position: absolute; /* grid override */ + padding: 0 15px 2px 0; /* grid override */ + line-height: 1.25em; + bottom: 0; + right: 0; + text-align: right; + font-size: 10pt; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.legacy.css b/www/wiki/extensions/Translate/resources/css/ext.translate.legacy.css new file mode 100644 index 00000000..62353931 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.legacy.css @@ -0,0 +1,65 @@ +/* Form at Special:Translate */ +.mw-sp-translate-error { + font-style: italic; + background-color: #ff0; +} + +/* This gets pretty far on wide screens... */ +.mw-sp-translate-settings input[ type='submit' ] { + float: right; +} + +/* For some reason a non-breaking space is not enough to keep the label + * with the dropdown. */ +.mw-sp-translate-settings label { + white-space: nowrap; +} + +.mw-sp-translate-table { + width: 100%; + border-width: 1px; + border-collapse: collapse; +} + +.mw-sp-translate-table th { + background-color: #b2b2ff; + border: 1px solid; +} + +.mw-sp-translate-table tr.orig { + background-color: #ffe2e2; +} + +.mw-sp-translate-table tr.new { + background-color: #e2ffe2; +} + +.mw-sp-translate-table tr.def { + background-color: #f0f0ff; +} + +.mw-sp-translate-table tr.ign { + background-color: #202020; +} + +.mw-sp-translate-table tr.opt { + background-color: #f2f200; +} + +.mw-sp-translate-table .untranslated { + background-color: #a2f290; +} + +.mw-sp-translate-table > tbody > tr > * { + vertical-align: top; + border: 1px solid #909090; +} + +.mw-translate-messagereviewbutton { + float: right; +} + +.mw-translate-messagereviewstatus { + clear: right; + text-align: right; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.loader.css b/www/wiki/extensions/Translate/resources/css/ext.translate.loader.css new file mode 100644 index 00000000..07d6dc18 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.loader.css @@ -0,0 +1,51 @@ +/* Loading indicator: */ + +@-webkit-keyframes tux-loading-indicator-spin { + from { + -webkit-transform: rotate( 0deg ); + } + + to { + -webkit-transform: rotate( 360deg ); + } +} + +@keyframes tux-loading-indicator-spin { + from { + transform: rotate( 0deg ); + } + + to { + transform: rotate( 360deg ); + } +} + +.tux-loading-indicator { + float: left; + background: transparent url( ../images/loading.gif ) right bottom no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/loading.svg ); + background-size: 100%; + -webkit-animation-name: tux-loading-indicator-spin; + -webkit-animation-duration: 1.5s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + animation-name: tux-loading-indicator-spin; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-timing-function: linear; + height: 34px; + width: 34px; + backface-visibility: hidden; +} + +.tux-loading-indicator--centered { + top: 50%; + left: 50%; + position: absolute; +} + +.tux-loading-indicator--stopped { + -webkit-animation: none; + animation: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.css b/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.css new file mode 100644 index 00000000..c668603d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.css @@ -0,0 +1,292 @@ +/* Default colors */ +.tux-messagelist { + color: #252525; + background-color: #F8F8F8; + max-width: 800px; +} + +.tux-message { + height: auto; + cursor: pointer; +} + +/* The "block" views of page mode and proofreading mode have 0 margin on + * .tux-message. To make the actual editor be of same width, set 0 margin on + * the open editor (overriding the -5px set by the grid) */ +.grid .tux-message.open { + margin: 0 auto; +} + +.tux-message-item { + line-height: 50px; + height: 50px; + overflow: hidden; + margin-right: 5px !important; + margin-left: 5px !important; + vertical-align: middle; + border-bottom: 1px solid #C9C9C9; + background: #FFFFFF; +} + +.tux-message-item.translated, +.tux-message-item.translated:hover, +.tux-message-item.proofread, +.tux-message-item.proofread:hover { + background-color: #F0F0F0; +} + +.tux-message-item:hover { + background-color: #F8F8F8; +} + +.tux-list-status span, +.tux-list-edit { + padding: 5px; + /* 15px space for icon */ + padding-left: 20px; + /* Do not combine these two, unless you also fix the + * tux-status-* styles below. That includes you, Siebrand ;) + */ + background-position: left; + background-repeat: no-repeat; +} + +.tux-info { + background-color: #F0F0F0; +} + +.tux-list-source { + white-space: nowrap; + text-overflow: ellipsis; + unicode-bidi: -webkit-isolate; + unicode-bidi: -moz-isolate; + unicode-bidi: isolate; +} + +.tux-list-translation { + white-space: nowrap; + color: #565656; + padding-left: 5px; + text-overflow: ellipsis; + unicode-bidi: -webkit-isolate; + unicode-bidi: -moz-isolate; + unicode-bidi: isolate; +} + +.tux-list-message { + overflow: hidden; + text-overflow: ellipsis; +} + +.tux-status-unsaved { + background-image: url(../images/label-pen.png); + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/label-pen.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/label-pen.svg); +} + +.tux-status-translated, +.tux-status-proofread { + background-image: url(../images/label-tick.png); + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/label-tick.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/label-tick.svg); +} + +.tux-status-fuzzy { + background-image: url(../images/label-clock.png); + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/label-clock.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/label-clock.svg); +} + +.tux-list-edit a { + background: transparent url(../images/action-edit.png) left center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/action-edit.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/action-edit.svg); + background-size: 18px 18px; + padding-left: 19px; +} + +.tux-messagetable-loader { + height: 75px; + color: #565656; + padding: 15px; + top: 0; + background: #F0F0F0 16px 50%; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 20px rgba(0, 0, 0, 0.1) inset; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 20px rgba(0, 0, 0, 0.1) inset; +} + +.tux-messagetable-loader-count { + padding: 0 0 5px 46px; + font-size: 25px; +} + +.tux-messagetable-loader-more { + padding-left: 46px; + font-size: 15px; +} + +.tux-action-bar { + background-color: #F0F0F0; + color: #252525; + -webkit-box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + transition: width 250ms; +} + +.tux-action-bar.floating { + border-top: 1px solid #DDD; + position: fixed; + bottom: 0; + z-index: 200; +} + +.tux-action-bar .tux-statsbar { + position: relative; + top: 30px; +} + +.tux-action-bar .toggle.button { + min-height: 40px; + font-size: 14px; + vertical-align: middle; + border-radius: 0; + text-shadow: none; + margin: 5px 0; + text-indent: 0; +} + +.tux-action-bar .tux-view-switcher { + padding: 0 5px; +} + +.tux-action-bar .tux-view-switcher .toggle.button { + padding: 0 2px 0 0; +} + +.tux-action-bar .tux-view-switcher .toggle.button:first-child { + border-radius: 3px 0 0 3px; + border-right: none; +} + +.tux-action-bar .tux-view-switcher .toggle.button:last-child { + border-radius: 0 3px 3px 0; + border-left: none; +} + +.tux-action-bar .tux-view-switcher .toggle.button:before { + content: ""; + height: 15px; + width: 25px; + display: inline-block; + vertical-align: bottom; +} + +.tux-action-bar .translate-mode-button { + width: 30%; +} + +.tux-action-bar .translate-mode-button:before { + background: transparent url(../images/view-list.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-list.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-list.svg); +} + +.tux-action-bar .translate-mode-button.down:before { + background: transparent url(../images/view-list-hi.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-list-hi.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-list-hi.svg); +} + +.tux-action-bar .page-mode-button { + width: 30%; +} + +.tux-action-bar .page-mode-button:before { + background: transparent url(../images/view-page.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-page.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-page.svg); +} + +.tux-action-bar .page-mode-button.down:before { + background: transparent url(../images/view-list-hi.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-page-hi.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-page-hi.svg); +} + +.tux-action-bar .proofread-mode-button { + width: 36%; +} + +.tux-action-bar .proofread-mode-button:before { + background: transparent url(../images/view-proofread.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-proofread.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-proofread.svg); +} + +.tux-action-bar .proofread-mode-button.down:before { + background: transparent url(../images/view-proofread-hi.png) center center no-repeat; + background-image: -webkit-linear-gradient(transparent, transparent), url(../images/view-proofread-hi.svg); + /* @embed */ + background-image: linear-gradient(transparent, transparent), url(../images/view-proofread-hi.svg); +} + +.tux-action-bar .toggle.button.down { + color: #FFF; + background: #252525; +} + +.ext-translate-container .tux-messagelist .tux-message-filter-result { + color: #252525; + line-height: 35px; + font-size: 15px; + vertical-align: middle; + border-bottom: 1px solid #C9C9C9; + margin: 0; + padding: 0 5px; +} + +.tux-message-filter-result.highlight { + background: #FFF5AA; +} + +.tux-message-filter-result .button { + float: right; + padding: 5px; + margin: 5px 0; + font-size: 15px; + vertical-align: middle; +} + +.tux-empty-list { + padding: 20px; +} + +.tux-empty-list-header { + font-size: 25px; + padding: 5px 0; +} + +.tux-empty-list-guide { + font-size: 15px; + color: #565656; + padding: 5px 0; +} + +.tux-empty-list-actions { + font-size: 15px; + padding: 8px 0; +} + +.tux-empty-list-actions a { + cursor: pointer; + margin: 0 10px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.less b/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.less new file mode 100644 index 00000000..d01b6b10 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.less @@ -0,0 +1,283 @@ +@import 'mediawiki.mixins'; + +/* Default colors */ +.tux-messagelist { + background-color: #f8f8f8; + color: #222; + max-width: 800px; +} + +.grid.ext-translate-container .row { + min-width: 300px !important; +} + +@media screen and ( max-width: 600px ) { + .grid.ext-translate-container .tux-messagelist .tux-list-message { + width: 100%; + } + + .tux-list-status, + .tux-list-edit { + display: none; + } +} + +.tux-message { + height: auto; + cursor: pointer; +} + +/* The "block" views of page mode and proofreading mode have 0 margin on + * .tux-message. To make the actual editor be of same width, set 0 margin on + * the open editor (overriding the -5px set by the grid) */ +.grid .tux-message.open { + margin: 0 auto; +} + +.tux-message-item { + line-height: 50px; + height: 50px; + overflow: hidden; + margin-right: 5px !important; + margin-left: 5px !important; + vertical-align: middle; + border-bottom: 1px solid #c9c9c9; + background: #fff; +} + +.tux-message-item.translated, +.tux-message-item.translated:hover, +.tux-message-item.proofread, +.tux-message-item.proofread:hover { + background-color: #f0f0f0; +} + +.tux-message-item:hover { + background-color: #f8f8f8; +} + +.tux-list-status span, +.tux-list-edit { + padding: 5px; + /* 15px space for icon */ + padding-left: 20px; + /* Do not combine these two, unless you also fix the + * tux-status-* styles below. That includes you, Siebrand ;) + */ + background-position: left; + background-repeat: no-repeat; +} + +.tux-info { + background-color: #f0f0f0; +} + +.tux-list-source { + white-space: nowrap; + text-overflow: ellipsis; + unicode-bidi: -webkit-isolate; + unicode-bidi: -moz-isolate; + unicode-bidi: isolate; +} + +.tux-list-translation { + color: #54595d; + white-space: nowrap; + padding-left: 5px; + text-overflow: ellipsis; + unicode-bidi: -webkit-isolate; + unicode-bidi: -moz-isolate; + unicode-bidi: isolate; +} + +.tux-list-message { + overflow: hidden; + text-overflow: ellipsis; +} + +.tux-status-unsaved { + .background-image( '../images/label-pen.svg' ); +} + +.tux-status-translated, +.tux-status-proofread { + .background-image( '../images/label-tick.svg' ); +} + +.tux-status-fuzzy { + .background-image( '../images/label-clock.svg' ); +} + +.tux-list-edit a { + .background-image( '../images/action-edit.svg' ); + background-position: left center; + background-repeat: no-repeat; + background-size: 18px 18px; + padding-left: 19px; +} + +.tux-messagetable-loader { + color: #54595d; + padding: 15px; + top: 0; + background: #f0f0f0 16px 50%; + box-shadow: 0 1px 4px rgba( 0, 0, 0, 0.3 ), 0 0 20px rgba( 0, 0, 0, 0.1 ) inset; +} + +.tux-messagetable-loader-info { + padding-left: 46px; + font-size: 25px; +} + +.tux-action-bar { + background-color: #f0f0f0; + color: #222; + box-shadow: 0 2px 6px rgba( 0, 0, 0, 0.3 ); + transition: width 250ms; +} + +@media screen and ( min-height: 500px ) { + .tux-action-bar.floating { + border-top: 1px solid #ddd; + position: fixed; + bottom: 0; + z-index: 200; + } +} + +.tux-action-bar .tux-statsbar { + position: relative; + top: 30px; +} + +.tux-action-bar .tux-view-switcher { + padding: 0 5px; +} + +.tux-action-bar button { + min-height: 40px; + font-size: 14px; + margin: 5px 0; + cursor: pointer; + background-color: #e6e6e6; + font-weight: bold; + line-height: 1; + background-image: linear-gradient( #f0f0f0, #e6e6e6 ); + border: 1px #c9c9c9 solid; +} + +.tux-action-bar button:hover { + background-color: #f0f0f0; + background-image: linear-gradient( #f8f8f8, #f0f0f0 ); +} + +.tux-action-bar button:active, +.tux-action-bar button.down { + background: #222; + color: #fff; +} + +.tux-action-bar button.disabled, +.tux-action-bar button.disabled:hover { + color: #c9c9c9; + cursor: default; + background-color: #f0f0f0; + border-color: #e3e3e3; +} + +.tux-view-switcher button { + padding: 0 2px 0 0; +} + +.tux-view-switcher button:first-child { + border-radius: 3px 0 0 3px; + border-right: 0; +} + +.tux-view-switcher button:last-child { + border-radius: 0 3px 3px 0; + border-left: 0; +} + +.tux-view-switcher button:before { + content: ''; + height: 15px; + width: 25px; + display: inline-block; + vertical-align: bottom; +} + +.tux-action-bar .translate-mode-button { + width: 30%; +} + +.tux-action-bar .translate-mode-button:before { + .background-image( '../images/view-list.svg' ); +} + +.tux-action-bar .translate-mode-button.down:before { + .background-image( '../images/view-list-hi.svg' ); +} + +.tux-action-bar .page-mode-button { + width: 30%; +} + +.tux-action-bar .page-mode-button:before { + .background-image( '../images/view-page.svg' ); +} + +.tux-action-bar .page-mode-button.down:before { + .background-image( '../images/view-page-hi.svg' ); +} + +.tux-action-bar .proofread-mode-button { + width: 36%; +} + +.tux-action-bar .proofread-mode-button:before { + .background-image( '../images/view-proofread.svg' ); +} + +.tux-action-bar .proofread-mode-button.down:before { + .background-image( '../images/view-proofread-hi.svg' ); +} + +.tux-message-filter-result { + color: #222; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #c9c9c9; + background: #fff5aa; + padding: 5px; + + & button { + background: inherit; + } +} + +.tux-empty-list { + padding: 20px; +} + +.tux-empty-list-header { + font-size: 25px; + padding: 5px 0; +} + +.tux-empty-list-guide { + color: #54595d; + font-size: 15px; + padding: 5px 0; +} + +.tux-empty-list-actions { + font-size: 15px; + padding: 8px 0; +} + +.tux-empty-list-actions a { + cursor: pointer; + margin: 0 10px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.messagewebimporter.css b/www/wiki/extensions/Translate/resources/css/ext.translate.messagewebimporter.css new file mode 100644 index 00000000..de322ca2 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.messagewebimporter.css @@ -0,0 +1,3 @@ +.mw-tmi-deleted .mw-tmi-diff .mw-tmi-new { + font-weight: normal; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.navitoggle.css b/www/wiki/extensions/Translate/resources/css/ext.translate.navitoggle.css new file mode 100644 index 00000000..afe45f4b --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.navitoggle.css @@ -0,0 +1,56 @@ +/** + * Introduces a toggle icon than can be used to hide navigation menu in vector + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +.tux-navitoggle { + background: no-repeat scroll right center transparent; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/expand-rtl.svg ); + height: 40px; + width: 20px; + position: absolute; + top: 0; + z-index: 10000000004; + cursor: pointer; +} + +.tux-navi-collapsed .tux-navitoggle { + background: no-repeat scroll right center transparent; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/expand-ltr.svg ); + left: 40px !important; +} + +.tux-navi-collapsed #mw-panel { + display: none; +} + +.tux-navi-collapsed #content { + margin-left: 0; +} + +.tux-navi-collapsed #left-navigation { + left: 0; +} + +.tux-navi-minilogo { + display: none; + position: absolute; + top: 0; + left: 0; +} + +.tux-navi-collapsed .tux-navi-minilogo { + display: block; +} + +.tux-navi-minilogo a { + width: 40px; + height: 40px; + background-size: 30px; + display: block; + background-repeat: no-repeat; + background-position: center center; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.pagemode.css b/www/wiki/extensions/Translate/resources/css/ext.translate.pagemode.css new file mode 100644 index 00000000..d1f1d274 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.pagemode.css @@ -0,0 +1,110 @@ +.ext-translate-container .tux-messagelist .tux-message-pagemode { + min-height: 50px; + margin: 0 auto; + vertical-align: middle; + background: #f8f8f8; +} + +.ext-translate-container .tux-messagelist .tux-message-pagemode .tux-message-item-compact { + padding: 35px 0; + line-height: 50px; + overflow: hidden; + margin-right: auto; + margin-left: auto; + vertical-align: middle; + border-bottom: 1px solid #f0f0f0; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + background: #fff; + max-width: 900px; +} + +.ext-translate-container .tux-messagelist .tux-message-pagemode .tux-message-item-compact:hover { + background: #fcfcfc; +} + +.ext-translate-container .tux-messagelist .tux-message-pagemode:first-child .tux-message-item-compact { + margin-top: 10px; + padding-top: 60px; + border-top: 1px solid #ddd; +} + +.ext-translate-container .tux-messagelist .tux-message-pagemode:last-child .tux-message-item-compact { + margin-bottom: 10px; + padding-bottom: 60px; + border-bottom: 1px solid #ddd; +} + +.tux-pagemode-source, +.tux-pagemode-translation { + word-wrap: break-word; +} + +.tux-message-pagemode.open .tux-pagemode-status, +.tux-message-pagemode.open .tux-pagemode-source, +.tux-message-pagemode.open .tux-pagemode-translation, +.tux-message-pagemode.open .tux-pagemode-action-block { + display: none; +} + +.tux-messagelist .tux-message-pagemode .tux-pagemode-source { + color: #54595d; + font-size: 16px; + line-height: 1.5em; + padding-right: 25px; + padding-left: 25px; +} + +.tux-messagelist .tux-message-pagemode .tux-pagemode-translation { + color: #222; + font-size: 16px; + line-height: 1.5em; + padding-left: 20px; +} + +.tux-pagemode-action-block { + top: -5px; + right: -5px; +} + +.tux-pagemode-status { + top: -10px; + height: 40px; +} + +.tux-pagemode-status.fuzzy { + background: left center no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/outdated-ltr.svg ); +} + +.tux-pagemode-status.untranslated { + background: left center no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/translate-ltr.svg ); +} + +.tux-pagemode-action { + background: right top no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/check-sprite-ltr.svg ); + height: 31px; + cursor: pointer; +} + +.tux-pagemode-action:hover { + background-position: right -31px; +} + +.tux-pagemode-edit { + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/edit-mark.svg ); + background-repeat: no-repeat; + background-position: right center; + height: 40px; + cursor: pointer; + visibility: hidden; +} + +.tux-message-pagemode:hover .tux-pagemode-edit { + visibility: visible; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.proofread.css b/www/wiki/extensions/Translate/resources/css/ext.translate.proofread.css new file mode 100644 index 00000000..69eddee1 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.proofread.css @@ -0,0 +1,179 @@ +.ext-translate-container .tux-messagelist .tux-message-proofread { + min-height: 50px; + margin: 0 auto; + vertical-align: middle; + background: #f8f8f8; +} + +.ext-translate-container .tux-messagelist .tux-message-proofread .tux-message-item-compact { + padding: 30px 0; + overflow: hidden; + margin-right: auto; + margin-left: auto; + vertical-align: middle; + border-bottom: 1px solid #f0f0f0; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + background: #fff; + max-width: 900px; +} + +.ext-translate-container .tux-messagelist .tux-message-proofread .tux-message-item-compact:hover { + background: #fcfcfc; +} + +.ext-translate-container .tux-messagelist .tux-message-proofread:first-child .tux-message-item-compact { + margin-top: 10px; + padding-top: 60px; + border-top: 1px solid #ddd; +} + +.ext-translate-container .tux-messagelist .tux-message-proofread:last-child .tux-message-item-compact { + margin-bottom: 10px; + padding-bottom: 60px; + border-bottom: 1px solid #ddd; +} + +.tux-proofread-source, +.tux-proofread-translation { + word-wrap: break-word; + white-space: pre-wrap; +} + +.tux-message-proofread.open .tux-proofread-status, +.tux-message-proofread.open .tux-proofread-source, +.tux-message-proofread.open .tux-proofread-translation, +.tux-message-proofread.open .tux-proofread-action-block { + display: none; +} + +.tux-messagelist .tux-message-proofread .tux-proofread-source { + color: #54595d; + font-size: 16px; + line-height: 1.5em; + padding-right: 25px; + padding-left: 25px; +} + +.tux-messagelist .tux-message-proofread .tux-proofread-translation { + color: #222; + font-size: 16px; + line-height: 1.5em; + padding-left: 20px; +} + +.tux-proofread-action-block { + top: -5px; + right: -5px; +} + +.ext-translate-container .tux-messagelist .tux-message-proofread.own-translation, +.ext-translate-container .tux-messagelist .tux-message-proofread.own-translation:hover { + background: #fbfbfb; +} + +.tux-messagelist.tux-hide-own .tux-message-proofread.own-translation { + display: none; +} + +.translated-by-self { + color: #72777d; + text-align: right; + font-size: 12px; + margin-right: 5px; + margin-left: auto; + width: 18px; + height: 18px; /* Icon height + 3px */ + background: top right no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/user-small.svg ); +} + +.tux-proofread-status { + top: -10px; +} + +.tux-proofread-status.fuzzy { + background: left center no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/outdated-ltr.svg ); + height: 40px; +} + +.tux-proofread-status.untranslated { + background: left center no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/translate-ltr.svg ); + height: 40px; +} + +.tux-proofread-action { + background-position: right top; + background-repeat: no-repeat; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/check-sprite-ltr.svg ); + /* There is 1px white row between each icon */ + height: 30px; + cursor: pointer; +} + +.tux-proofread-action:hover { + background-position: right -31px; +} + +.tux-proofread-action.accepted { + cursor: default; + background-position: right -62px; +} + +.proofread-by-others .tux-proofread-action { + background-position: right -124px; +} + +.proofread-by-others .tux-proofread-action:hover { + background-position: right -155px; +} + +.proofread-by-others .tux-proofread-action.accepted { + cursor: default; + background-position: right -186px; +} + +.tux-proofread-edit { + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/edit-mark.svg ); + background-repeat: no-repeat; + background-position: right center; + height: 50px; + cursor: pointer; + visibility: hidden; + text-align: right; +} + +.tux-message-proofread:hover .tux-proofread-edit { + visibility: visible; +} + +.tux-proofread-edit-label { + color: #72777d; + position: relative; + display: inline-block; + font-size: 13px; + padding-top: 30px; + padding-right: 2px; +} + +.tux-proofread-count { + color: #72777d; + font-size: 15px; + padding-right: 5px; + text-align: right; +} + +.tux-proofread-count:before { + content: ''; + display: inline-block; + background: left bottom no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/check-small.svg ); + height: 12px; + width: 14px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.quickedit.css b/www/wiki/extensions/Translate/resources/css/ext.translate.quickedit.css new file mode 100644 index 00000000..37b9c4d3 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.quickedit.css @@ -0,0 +1,108 @@ +.mw-sp-translate-edit-fields a { + color: #00f; +} + +.mw-translate-tmsug { + margin-bottom: 0.2em; +} + +.mw-sp-translate-edit-fields fieldset { + line-height: normal; + margin: 0; + border: 1px solid #dbdbdb; + /* Browsers suck + max-height: 250px; */ + overflow: auto; +} + +.mw-sp-translate-edit-fields legend { + background-color: transparent; +} + +.mw-translate-edit-extra { + border-bottom: 1px solid #000; +} + +.mw-translate-legend { + border-left: 1px solid #000; + float: right; + margin-left: 1em; + padding-left: 5pt; + font-weight: bold; +} + +/* Align the adder according to the target language */ +/* @noflip */ +.mw-translate-adder-ltr { + float: left; + padding-right: 1px; +} + +/* @noflip */ +.mw-translate-adder-rtl { + float: right; + padding-left: 1px; +} + +.mw-translate-sep { + margin-bottom: 1ex; + margin-top: 0.5ex; +} + +.mw-translate-edit-deftext { + /* + * Some browsers will render the monospace text too small, namely Firefox, Chrome and Safari. + * Specifying any valid, second value will trigger correct behavior without forcing a different font. + * See docs/uidesign/monospace.html of MediaWiki core source code. + */ + font-family: monospace, monospace; +} + +.mw-translate-edit-area { + padding: 0; + width: 100%; +} + +/* Buttons */ +.mw-translate-save { + font-weight: bold; +} + +.mw-translate-history { + float: right; +} + +.mw-ajax-dialog #summary { + width: 5em; +} + +/* Blue tints for translate fieldsets */ +.mw-sp-translate-edit-inother { + background-color: #f8f8ff; +} + +.mw-sp-translate-in-other-small { + background-color: #f0f8ff; +} + +.mw-sp-translate-in-other-big { + background-color: #f0f8ff; +} + +.mw-sp-translate-message-documentation { + background-color: #ebebeb; +} + +.mw-sp-translate-edit-definition { + background-color: #eaf3fc; +} + +.mw-translate-inputs { + overflow: auto; /* Fix "100%" width after floats issue */ + padding: 2px; /* Avoid random scrollbars (browsers suck) */ +} + +.mw-translate-bottom { + clear: both; + margin-top: -3px; /* Reduce excess whitespace */ +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.aggregategroups.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.aggregategroups.css new file mode 100644 index 00000000..98c80b09 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.aggregategroups.css @@ -0,0 +1,53 @@ +span.tp-aggregate-remove-ag-button, +span.tp-aggregate-remove-button { + background: no-repeat scroll left center transparent; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/remove.svg ); + padding: 10px; + cursor: pointer; +} + +span.tp-aggregate-edit-ag-button { + background: no-repeat scroll left center transparent; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/action-edit.svg ); + background-size: 18px 18px; + padding: 10px; + cursor: pointer; +} + +a.tpt-add-new-group { + background: no-repeat scroll left center transparent; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/add.svg ); + padding-left: 20px; +} + +input.tp-aggregategroup-add-name { + width: 250px; +} + +input.tp-aggregategroup-add-description { + width: 500px; +} + +div.hidden { + display: none; +} + +.tp-aggregategroup-edit-name { + width: 250px; +} + +.tp-aggregategroup-edit-description { + width: 500px; +} + +.client-nojs .tpt-add-new-group, +.client-nojs .tp-aggregate-edit-ag-button, +.client-nojs .tp-aggregate-remove-button, +.client-nojs .tp-aggregate-remove-ag-button, +.client-nojs .mw-tpa-group input { + display: none; +} + +.client-js .tux-nojs { + display: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.languagestats.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.languagestats.css new file mode 100644 index 00000000..79bf9a18 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.languagestats.css @@ -0,0 +1,21 @@ +.mw-sp-translate-table.wikitable tr:hover td { + background: white; +} + +.groupexpander-all { + text-align: right; +} + +.groupexpander { + float: right; +} + +.statstable .expanded, +.statstable .expanded a { + cursor: n-resize; +} + +.statstable .collapsed, +.statstable .collapsed a { + cursor: s-resize; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.managegroups.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.managegroups.css new file mode 100644 index 00000000..e919e1e9 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.managegroups.css @@ -0,0 +1,20 @@ +.mw-translate-smg-change { + padding-bottom: 1em; + margin-bottom: 2em; +} + +.mw-translate-smg-submit { + font-size: 5em; + margin: auto; + width: 80%; + display: block; +} + +.diff-lineno { + display: none; +} + +.mw-translate-smg-header td { + font-size: 200%; + font-weight: bold; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.managetranslatorsandbox.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.managetranslatorsandbox.css new file mode 100644 index 00000000..4f5aa752 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.managetranslatorsandbox.css @@ -0,0 +1,284 @@ +/** + * CSS for special page. + * @author Niklas Laxström + * @author Sucheta Ghoshal + * @author Pau Giner + * @license GPL-2.0-or-later + */ + +/* Hide the page title to give more space for the content */ +#firstHeading { + display: none; +} + +/* Panes */ +.filter.pane, +.search.pane { + border-bottom: 1px solid #575656; + height: 2em; + line-height: 50px; + font-size: 24px; +} + +.grid .search.pane { + background: no-repeat scroll left top transparent; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/search.svg ); + background-size: 25px; + padding-left: 25px; +} + +.request-filter-box { + font-size: 14px; + height: 28px; + width: 100%; + border: 1px solid #c9c9c9; + padding: 0 5px; +} + +.tsb-body { + border: 1px solid #c9c9c9; + border-top: 0; +} + +.requests.pane, +.details.pane { + border-top: 1px solid #d3d2d2; +} + +.requests.pane .requests-list, +.details.pane { + overflow: auto; +} + +.grid .details.pane { + border-left: 1px solid #ddd; + left: -1px; + padding: 5px 20px 10px 20px; +} + +/* Requests pane */ +.grid .requests.pane, +.grid .requests.pane .request, +.grid .requests.pane .request-header { + margin: 0; + padding: 0; +} + +.request { + border-right: 1px solid #c9c8c8; + border-bottom: 1px solid #c9c8c8; + cursor: pointer; +} + +.request:last-child { + border-bottom: 0; +} + +.request:hover { + background-color: #fafafa; +} + +.request.selected { + background-color: #f0f0f0; +} + +.grid .request .amount { + height: 100%; + font-size: 3em; + color: #96989a; + background-color: #f7f8f8; + padding: 20px 5px; + text-align: center; +} + +.request .tsb-header, +.request .request-selector { + height: 30px; + line-height: 30px; + font-size: 16px; + white-space: nowrap; +} + +.request .email, +.request .signup-age { + height: 20px; + line-height: 20px; + font-size: 12px; + color: #6c6d70; + white-space: nowrap; +} + +.request .tsb-header, +.request .username, +.request .email { + padding-left: 10px; + text-overflow: ellipsis; +} + +.request .signup-age { + text-align: center; + overflow: hidden; +} + +.grid .request .request-info, +.grid .request .approval { + padding-top: 5px; +} + +/* Details pane */ + +.tsb-details-no-translations { + color: #72777d; +} + +.signup-comment-label { + color: #e85355; + font-size: 14px; +} + +.signup-comment-text { + color: #222; + font-size: 16px; + line-height: 1.5em; + padding: 5px 0; +} + +.details.pane > .row { + padding-top: 15px; +} + +.details.pane .tsb-header { + font-size: 30px; + font-weight: lighter; +} + +.details.pane .reminder-email { + color: #6c6d70; + font-size: 14px; + padding-top: 15px; +} + +.details.pane .reminder-email .send-reminder { + padding-left: 1em; + padding-right: 1em; +} + +.details.pane .languages { + color: #6c6d70; + font-size: 16px; + font-weight: lighter; + padding-top: 10px; +} + +.details.pane .languages span { + margin-right: 2em; +} + +.details.pane .actions { + font-size: 22px; +} + +.actions button { + margin-right: 1.5em; +} + +.request-header { + color: #72777d; + border-right: 1px solid #c9c9c9; + border-bottom: 1px solid #aaa; + line-height: 40px; + background-color: #f8f8f8; +} + +.request-footer { + color: #72777d; + border-right: 1px solid #c9c9c9; + border-top: 1px solid #aaa; + line-height: 40px; + padding: 0 5px; + background-color: #f8f8f8; +} + +.request-footer .selected-counter, +.request-footer .older-requests-indicator { + unicode-bidi: -moz-isolate; + unicode-bidi: -webkit-isolate; + unicode-bidi: isolate; +} + +.clear-language-selector, +.language-selector { + margin: 10px 0; + border-radius: 3px; + background: #f8f8f8; + border: 1px solid #ccc; + cursor: pointer; + font-size: 1em; + display: block; + float: left; +} + +.clear-language-selector { + border-radius: 0 3px 3px 0; + border-left: 0; +} + +.language-selector.selected { + cursor: default; + border-radius: 3px 0 0 3px; + max-width: 80%; + max-height: 40px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.language-selector:hover { + border-color: #aaa; +} + +.language-selector.unselected:after { + content: ''; + border-top: 4px solid #aaa; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display: inline-block; + vertical-align: middle; + margin-left: 0.5em; +} + +textarea.body { + height: 300px; +} + +.translations .row { + border-bottom: 1px solid #c9c8c8; + padding: 10px; + font-size: 16px; + word-wrap: break-word; +} + +.translations .title { + font-size: 16px; + background-color: #f7f8f8; + font-weight: bold; +} + +.translations .info { + font-size: 12px; + color: #6c6d70; + /* + * Align autonyms consistently. + * The direction is set according to the language on the frontend + * and the alignment is flipped according to the user language. + */ + text-align: left; +} + +.client-nojs .grid { + display: none; +} + +.client-js .tux-nojs { + display: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagemigration.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagemigration.css new file mode 100644 index 00000000..06c061cf --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagemigration.css @@ -0,0 +1,76 @@ +.mw-tpm-sp-unit__source, +.mw-tpm-sp-unit__target { + overflow-y: auto; + height: 150px; + border: 2px solid #808080; +} + +#title { + width: 35%; +} + +.hide { + display: none; +} + +.mw-tpm-sp-unit:last-child .mw-tpm-sp-action--swap, +.mw-tpm-sp-unit:last-child .mw-tpm-sp-action--add { + display: none; +} + +.mw-tpm-sp-unit { + padding-bottom: 10px; +} + +.grid .mw-tpm-sp-unit textarea { + padding: 10px; +} + +.mw-tpm-sp-unit__source, +.mw-tpm-sp-unit__target, +.mw-tpm-sp-unit__actions { + height: 150px; +} + +.mw-tpm-sp-action { + width: 25px; + height: 150px; + cursor: pointer; + display: inline-block; + margin-left: 20px; + background-position: center; + background-repeat: no-repeat; + background-size: 25px 25px; +} + +.mw-tpm-sp-action--delete { + background-image: linear-gradient( transparent, transparent ), url( ../images/trash_darkgray.svg ); +} + +.mw-tpm-sp-action--swap { + background-image: linear-gradient( transparent, transparent ), url( ../images/switch.svg ); +} + +.mw-tpm-sp-action--add { + background-image: linear-gradient( transparent, transparent ), url( ../images/plus_darkgray.svg ); +} + +.mw-tpm-sp-error__message { + font-size: 0.9em; + word-wrap: break-word; + color: #c00; + border: 1px solid #fac5c5; + background-color: #fae3e3; +} + +.mw-tpm-sp-instructions { + margin: 0.7em 0; +} + +.client-nojs .grid { + display: none; +} + +.client-js .tux-nojs { + display: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagepreparation.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagepreparation.css new file mode 100644 index 00000000..d68d6072 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagepreparation.css @@ -0,0 +1,11 @@ +#page { + width: 35%; +} + +.client-nojs .grid { + display: none; +} + +.client-js .tux-nojs { + display: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagetranslation.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagetranslation.css new file mode 100644 index 00000000..09cacb60 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.pagetranslation.css @@ -0,0 +1,27 @@ +.mw-tpt-sp-section { + padding-bottom: 3ex; +} + +.mw-tpt-sp-section-type-old { + opacity: 0.65; +} + +.mw-tpt-sp-legend { + font-weight: bold; + font-size: 110%; +} + +.mw-tpt-sp-content { + font-size: small; + padding-left: 2em; + padding-right: 2em; +} + +.ui-autocomplete { + max-height: 100px; + overflow-y: auto; + /* prevent horizontal scrollbar */ + overflow-x: hidden; + /* add padding to account for vertical scrollbar */ + padding-right: 20px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.searchtranslations.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.searchtranslations.css new file mode 100644 index 00000000..c866a818 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.searchtranslations.css @@ -0,0 +1,165 @@ +/** + * @author Niklas Laxström + * @author Pau Giner + * @since 2013-01-10 + */ + +.tux-searchpage .tux-selectedbox .facet-item { + background-color: #ededed; + margin-right: 5px; +} + +.tux-searchpage .tux-searchboxform .tux-selectedbox, +.tux-searchpage .searchcontent .facets { + padding: 0 15px 0 5px; +} + +/* Facets */ +.tux-searchpage .facet { + color: #222; + font-size: 24px; + padding: 20px 0 10px 0; +} + +.tux-searchpage .facet-item { + font-size: 16px; + padding: 4px 5px; +} + +.tux-searchpage .facet-item:hover { + background: #f8f8f8; + cursor: pointer; +} + +.grid.tux-searchpage .facet-item:first-child { + margin-top: 10px; +} + +.tux-searchpage .facet-item .facet-count { + color: #aaa; + float: right; + margin-right: 5%; +} + +.tux-searchpage .facet-level-0 { + font-size: 16px; +} + +.tux-searchpage .facet-level-1 { + font-size: 14px; +} + +.tux-searchpage .facet-level-2 { + font-size: 12px; +} + +.tux-searchpage .facet-level-3 { + font-size: 10px; +} + +.tux-searchpage .facet-item .facet-name.selected { + font-weight: bold; +} + +.tux-searchpage .facet-item a:visited, +.tux-searchpage .facet-item a:link { + color: #0645ad; +} + +/* Results */ +.tux-searchpage .count { + color: #54595d; + font-size: 16px; + padding-bottom: 10px; +} + +.tux-searchpage .searchcontent .results { + color: #222; + padding-left: 10px; +} + +.tux-searchpage .results .tux-text { + text-align: left; + font-size: 16px; + padding-top: 20px; +} + +.tux-searchpage .results .tux-title { + text-align: left; + color: #54595d; +} + +/* Pagination links */ +.tux-searchpage .results .tux-pagination-line { + color: #eee; + font-size: 1px; +} + +.tux-searchpage .results .tux-pagination-links { + font-size: 20px; + padding-top: 20px; + text-align: center; +} + +/* Search area */ +.tux-searchpage .searchinput { + padding: 10px 0; +} + +.tux-searchpage .searchinput .searchinputbox { + width: 60%; + display: inline-block; + margin-right: 5px; +} + +.tux-search-operators { + margin: 5px 0; +} + +h1.firstHeading { + display: none; +} + +.translate-search-more-groups, +.translate-search-more-languages { + background-color: #f0f0f0; + font-size: 16px; + cursor: pointer; + padding: 0 6px; + border-radius: 2px; + border: 1px solid #eee; +} + +.translate-search-more-groups:hover, +.translate-search-more-languages:hover { + border: 1px solid #ccc; + text-decoration: none; +} + +.translate-search-more-groups-info, +.translate-search-more-languages-info { + color: #72777d; + font-size: 14px; + padding: 0 8px; +} + +/* Override tabs */ +.tux-searchpage .tux-messagetable-header .seven { + width: 100%; +} + +.tux-searchpage .tux-message-selector .more ul { + width: auto; +} + +.tux-searchpage .tux-message-selector .more ul a { + white-space: pre-wrap; +} + +.tux-searchpage .successbox { + margin-left: 25%; +} + +.tux-search-highlight { + background-color: #c9c9c9; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.supportedlanguages.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.supportedlanguages.css new file mode 100644 index 00000000..0bab7abe --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.supportedlanguages.css @@ -0,0 +1,21 @@ +.mw-translate-spsl-translators { + text-align: justify; + line-height: 200%; +} + +.mw-special-SupportedLanguages h2 { + font-weight: bold; + margin-top: 2em; +} + +.tagcloud { + line-height: 200%; + margin: 5em; + text-align: center; +} + +.tagcloud .tag { + white-space: nowrap; + margin: 0.5ex; + color: #000; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.translate.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.translate.css new file mode 100644 index 00000000..8bb45be8 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.translate.css @@ -0,0 +1,220 @@ +/* + * Breadcrumb for message group selector + */ +.tux-breadcrumb { + color: #54595d; + margin-bottom: 10px; + font-size: 14pt; + line-height: 1.25em; +} + +/* Arrow between breadcrumb items */ +.tux-breadcrumb .grouplink + .grouplink:before { + border-left: 4px solid #777; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + content: ''; + display: inline-block; + vertical-align: middle; + position: relative; + left: -5px; +} + +/* This applies to all items in the breadcrumb */ +.tux-breadcrumb .grouptitle { + float: left; + padding: 0 6px 0 0; +} + +/* Not all of these are links, so name is wrong besides being too generic. + * This excludes the first item which says "message group". */ +.tux-breadcrumb .grouplink { + padding: 0 6px; +} + +/* Color clickable groups to look as links */ +.tux-breadcrumb__item--aggregate { + cursor: pointer; + color: #0645ad; +} + +/* Language selector */ +.ext-translate-language-selector-label { + color: #54595d; +} + +.ext-translate-language-selector { + float: right; + text-align: right; + border: medium none; + font-size: 14pt; + font-weight: normal; + line-height: 1.25em; + padding-bottom: 3px; + padding-left: 15px; + padding-top: 1.25em; +} + +/* The triangle shaped down-pointing callout after the language name + * in the target language selector + */ +.ext-translate-language-selector .uls:after { + margin-left: 4px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #0645ad; + content: ''; + display: inline-block; + vertical-align: middle; +} + +.ext-translate-language-selector .uls { + color: #0645ad; + cursor: pointer; + min-height: 1px; + position: relative; +} + +.tux-message-selector { + font-size: 14px; + margin: 0; + padding: 6px 0 0 0; + list-style: none; +} + +.tux-message-selector .column { + border: medium none; + font-size: 14pt; + cursor: pointer; + font-weight: normal; + line-height: 1.25em; + padding-bottom: 3px; + padding-left: 15px; + padding-top: 1.25em; + top: 1px; + display: block; + position: relative; + float: left; + margin-bottom: 0; +} + +.tux-message-selector .more { + padding: 0 40px 0 5px; +} + +.tux-message-selector .more ul { + display: none; + cursor: default; + border: 1px solid #777; + border-top: 2px solid transparent; + padding: 3px 5px 10px 5px; + width: 300px; + z-index: 10; + background: #fff; + top: 100%; + left: -5px; +} + +.tux-message-selector .more ul a { + display: block; + white-space: nowrap; + margin-left: 1px; +} + +.tux-message-selector .more:hover ul { + display: block; + position: absolute; +} + +.tux-message-selector .more:hover li { + float: none; +} + +.tux-message-selector li.selected { + border-bottom: 2px solid #36c; +} + +.tux-message-selector li.selected a { + color: #36c; +} + +.tux-message-selector li a { + color: #54595d; + text-decoration: none; + white-space: nowrap; + margin-left: 1px; +} + +.tux-message-selector label { + color: #54595d; + font-size: 12pt; + top: 1px; +} + +.tux-editor-header { + color: #222; + font-size: 14px; + padding-bottom: 20px; +} + +.tux-editor-header .description { + margin: 5px 0; +} + +.group-warning { + background: #fff5aa; + padding: 5px; + margin: 5px 0; +} + +.group-warning:empty { + display: none; +} + +.tux-messagetable-header { + padding-top: 5px; + border-bottom: 1px solid #777; + /* @noflip */ + box-shadow: 0 3px 3px -3px rgba( 0, 0, 0, 0.5 ); + font-size: 14px; + margin: 0; + list-style: none; + transition: width 250ms; +} + +@media screen and ( min-height: 600px ) { + .tux-messagetable-header.floating { + background: #fff; + position: fixed; + padding-top: 5px; + top: 0; + z-index: 200; + } + + .tux-messagetable-header.floating + .tux-messagelist { + margin-top: 50px; + } +} + +.tux-message-filter-box { + font-size: 14px; + height: 28px; + border: 1px solid #c9c9c9; + width: 100%; + padding: 0 5px; +} + +.tux-message-filter-wrapper { + background: no-repeat scroll left center transparent; + background-image: /* @embed */ linear-gradient( transparent, transparent ), url( ../images/search.svg ); + background-size: 25px; + padding-left: 30px; +} + +.client-nojs .tux-messagetable-header { + display: none; +} + +.client-js .tux-nojs { + display: none; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.special.translationstash.css b/www/wiki/extensions/Translate/resources/css/ext.translate.special.translationstash.css new file mode 100644 index 00000000..f451cb53 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.special.translationstash.css @@ -0,0 +1,98 @@ +/** + * @author Santhosh Thottingal + * @license GPL-2.0-or-later + * @since 2013.10 + */ + +h1.firstHeading { + display: none; +} + +h1 { + text-decoration: none; + border-bottom: 0; +} + +.translate-welcome-header > p { + color: #54595d; + font-size: 1.2em; +} + +.translate-welcome-header { + background-color: #f8f8f8; + background-image: linear-gradient( #fafafa, #f8f8f8 ); + border-bottom: 1px solid #eee; + padding: 10px; +} + +.limit-reached > p { + color: #57585a; + font-size: 1.2em; +} + +.limit-reached { + margin-top: 10px; + background-color: #fbf9ce; + padding: 10px; +} + +.translate-stash-control { + color: #54595d; + font-size: 1.5em; + padding: 35px 10px 20px 5px; +} + +.ext-translate-language-selector-label { + color: #54595d; +} + +.ext-translate-language-selector { + text-align: right; +} + +/* The triangle shaped down-pointing callout after the language name + * in the target language selector + */ +.ext-translate-language-selector:after { + margin-left: 4px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #0645ad; + content: ''; + display: inline-block; + vertical-align: middle; +} + +.tux-messagelist { + padding: 0 20px; + background: none; +} + +.tux-loading-indicator { + position: relative; + top: 50%; + left: 50%; +} + +.messagekey { + visibility: hidden; +} + +.message-desc-control, +.layout-actions .close, +.tux-message-item, +.tux-message-item.translated.hide { + display: none; +} + +.tux-message:first-child .tux-message-item { + border-top: 1px solid #c9c9c9; +} + +.tux-message-item.translated { + display: block; +} + +.sourcemessage { + top: -10px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.statsbar.css b/www/wiki/extensions/Translate/resources/css/ext.translate.statsbar.css new file mode 100644 index 00000000..32df390c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.statsbar.css @@ -0,0 +1,38 @@ +.tux-statsbar { + max-width: 400px; + padding: 0; + height: 5px; + background-color: #bbb; +} + +.tux-statsbar span { + height: 5px; + float: left; + padding: 0; + transition: width 1s; +} + +.tux-statsbar .tux-proofread { + background-color: #00af89; +} + +.tux-statsbar .tux-translated { + background-color: #2a4b8d; +} + +.tux-statsbar .tux-fuzzy { + background-color: #fc3; +} + +.tux-statsbar .tux-untranslated { + display: none; +} + +.tux-statsbar-info { + color: #72777d; + font-weight: normal; + line-height: 1.25em; + font-size: 10pt; + position: absolute; + padding-top: 5px; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.statstable.less b/www/wiki/extensions/Translate/resources/css/ext.translate.statstable.less new file mode 100644 index 00000000..a33400f1 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.statstable.less @@ -0,0 +1,63 @@ +.groupexpander-all { + text-align: right; +} + +.groupexpander { + float: right; +} + +.statstable { + border-collapse: collapse; + border-style: hidden; + box-shadow: 0 1px 1px rgba( 0, 0, 0, 0.15 ); + + .expanded, + .expanded a { + cursor: n-resize; + } + + .collapsed, + .collapsed a { + cursor: s-resize; + } + + /* Small zebra rows effect */ + > * > tr { + &.tux-statstable-even > td { + background-color: #f8f9fa; + } + + &:hover > td { + // Work-around a Firefox issue: + // https://bugzilla.mozilla.org/show_bug.cgi?id=688556 + background-clip: padding-box; + filter: brightness( 0.95 ); + } + + > th { + background-color: #eaecf0; + border: 1px solid #fff; + padding: 0.5em; + font-weight: normal; + letter-spacing: 1px; + } + + > td { + background-color: #fff; + padding: 0.5em; + border-left: 1px solid #f8f9fa; + border-right: 1px solid #f8f9fa; + } + + /* Align numbers to the right */ + > td:nth-child( n+2 ):nth-child( -n+6 ) { + text-align: right; + padding-right: 1em; + } + + /* De-emphasize 0% oudated */ + > td:nth-child( 6 )[ data-sort-value='0.00000' ] { + color: #54595d; + } + } +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.tabgroup.css b/www/wiki/extensions/Translate/resources/css/ext.translate.tabgroup.css new file mode 100644 index 00000000..ef319755 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.tabgroup.css @@ -0,0 +1,8 @@ +/* Heading not needer for special pages in tab group */ +#firstHeading { + display: none; +} + +#left-navigation .selected { + font-weight: bold; +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.tag.languages.css b/www/wiki/extensions/Translate/resources/css/ext.translate.tag.languages.css new file mode 100644 index 00000000..3f1c7ebf --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.tag.languages.css @@ -0,0 +1,71 @@ +.mw-pt-languages { + background-color: #f8f9fa; + display: table; + border: 1px solid #a2a9b1; + box-sizing: border-box; + border-collapse: collapse; + line-height: 1.2; + width: 100%; + clear: both; + overflow: auto; +} + +.mw-pt-languages-label { + background-color: #eaecf0; + display: table-cell; + padding: 0.5em; + font-weight: bold; + white-space: nowrap; +} + +.mw-pt-languages-list { + display: table-cell; + padding-left: 0.5em; + padding-bottom: 0.7em; +} + +.mw-pt-languages-list a { + white-space: nowrap; +} + +.mw-pt-languages-selected, +.mw-pt-languages-ui { + font-weight: bold; +} + +.mw-pt-progress { + padding-right: 11px; + background: transparent right center no-repeat; + background-size: 9px 9px; +} + +/* Need very high specificity to override skin styles in the sidebar */ +#mw-panel .portal .body .mw-pt-progress--none a, +.interwiki-x-pagetranslation.mw-pt-progress--none a { + color: #ba0000; +} + +#mw-panel .portal .body .mw-pt-progress--none a:visited, +.interwiki-x-pagetranslation.mw-pt-progress--none a:visited { + color: #a55858; +} + +.mw-pt-progress--stub { + background-image: url( ../images/prog-1.png ); +} + +.mw-pt-progress--low { + background-image: url( ../images/prog-2.png ); +} + +.mw-pt-progress--med { + background-image: url( ../images/prog-3.png ); +} + +.mw-pt-progress--high { + background-image: url( ../images/prog-4.png ); +} + +.mw-pt-progress--complete { + background-image: url( ../images/prog-5.png ); +} diff --git a/www/wiki/extensions/Translate/resources/css/ext.translate.workflowselector.css b/www/wiki/extensions/Translate/resources/css/ext.translate.workflowselector.css new file mode 100644 index 00000000..0181650d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/css/ext.translate.workflowselector.css @@ -0,0 +1,53 @@ +.tux-workflow-status { + background: #eee; + color: #222; + border: 1px solid #ddd; + cursor: pointer; + display: inline-block; + padding: 2px 4px; + margin-top: 5px; + min-width: 150px; +} + +.tux-workflow-status:hover { + border: 1px solid #c9c9c9; +} + +.tux-workflow-status-triangle { + float: right; +} + +.tux-workflow-status-triangle:after { + margin: 3px; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 3px solid #555; + content: ''; + display: inline-block; + vertical-align: middle; +} + +ul.tux-workflow-status-selector { + min-width: 150px; + margin-top: -1px; +} + +.tux-workflow-status-selector li { + color: #54595d; + display: block; + font-size: 14px; + padding: 0 2px; +} + +.tux-workflow-status-selector li.changeable:hover { + background-color: #f0f0f0; + color: #222; + cursor: pointer; +} + +.tux-workflow-status-selector li.selected { + background: right no-repeat; + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( ../images/label-tick.svg ); + color: #222; +} diff --git a/www/wiki/extensions/Translate/resources/images/action-edit.png b/www/wiki/extensions/Translate/resources/images/action-edit.png Binary files differnew file mode 100644 index 00000000..362cb6bd --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/action-edit.png diff --git a/www/wiki/extensions/Translate/resources/images/action-edit.svg b/www/wiki/extensions/Translate/resources/images/action-edit.svg new file mode 100644 index 00000000..a4c351c6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/action-edit.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <path d="M9.825 1.975l-6.218 8.888h-.015l-.19 2.164 1.977-.92 6.217-8.89-1.772-1.244z" fill="#36c"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/add.png b/www/wiki/extensions/Translate/resources/images/add.png Binary files differnew file mode 100644 index 00000000..b2863b4e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/add.png diff --git a/www/wiki/extensions/Translate/resources/images/add.svg b/www/wiki/extensions/Translate/resources/images/add.svg new file mode 100644 index 00000000..3cb491c0 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/add.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient id="c"> + <stop offset="0" stop-color="#8ccb81"/> + <stop offset="1" stop-color="#65ab55"/> + </linearGradient> + <linearGradient id="b"> + <stop offset="0" stop-color="#dcf1d8"/> + <stop offset="1" stop-color="#89be78"/> + </linearGradient> + <linearGradient id="a"> + <stop offset="0" stop-color="#34812c" stop-opacity=".957"/> + <stop offset="1" stop-color="#87b870" stop-opacity=".957"/> + </linearGradient> + <linearGradient id="f" x1="4.551" x2="11.459" y1="4.433" y2="11.341" xlink:href="#c" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.038) scale(1.002)"/> + <linearGradient id="d" x1="2.583" x2="12.758" y1="2.521" y2="13.001" xlink:href="#b" gradientUnits="userSpaceOnUse" gradientTransform="translate(.253 .127)"/> + <linearGradient id="e" x1="12.758" x2="2.583" y1="13.001" y2="2.521" xlink:href="#a" gradientUnits="userSpaceOnUse" gradientTransform="translate(.253 .127)"/> + </defs> + <path fill="url(#d)" fill-rule="evenodd" stroke="url(#e)" d="M14.9 7.95a6.85 6.85 0 1 1-13.7 0 6.85 6.85 0 1 1 13.7 0z"/> + <path fill="url(#f)" d="M13 8A5 5 0 1 1 3 8a5 5 0 1 1 10 0z"/> + <path fill="#fff" fill-opacity=".957" d="M7.032 5v2.042H5v1.995h2.063V11h2V9.005H11V7.01H9.032V5.017z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/check-small.png b/www/wiki/extensions/Translate/resources/images/check-small.png Binary files differnew file mode 100644 index 00000000..7217f8d6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-small.png diff --git a/www/wiki/extensions/Translate/resources/images/check-small.svg b/www/wiki/extensions/Translate/resources/images/check-small.svg new file mode 100644 index 00000000..9dd46c85 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-small.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"> + <path fill="#a2a9b1" d="M9.187 0L4.34 8.35l-1.897-1.4-1.49 1.984 3.037 2.248L5.1 12l.7-1.197 5.52-9.577L9.186 0z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.png b/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.png Binary files differnew file mode 100644 index 00000000..9a7300e2 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.png diff --git a/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.svg b/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.svg new file mode 100644 index 00000000..aee115fc --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.svg @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="44" height="248"> + <path fill="#eaecf0" d="M44 62H9.978l-9.8 15 9.8 15H44z" overflow="visible"/> + <path fill="#3e3e3e" d="M34.794 64.844L24.504 82.57l-4.03-2.974-3.16 4.215 6.447 4.775 2.357 1.735 1.488-2.542 11.714-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#c8dbf3" d="M44 93H9.978l-9.8 15 9.8 15H44z" overflow="visible"/> + <path fill="#36c" stroke="#36c" stroke-width=".75" d="M34.794 95.844l-10.29 17.727-4.03-2.973-3.16 4.215 6.447 4.774 2.357 1.735 1.488-2.54L39.32 98.45l-4.525-2.605z" overflow="visible"/> + <path fill="#eaecf0" d="M44 0H9.978l-9.8 15 9.8 15H44z" overflow="visible"/> + <path fill="#fff" stroke="#a2a9b1" stroke-width=".75" d="M34.794 2.844L24.504 20.57l-4.03-2.974-3.16 4.215 6.447 4.775 2.357 1.735 1.488-2.542L39.32 5.448l-4.525-2.604z" overflow="visible"/> + <path fill="#c8dbf3" d="M44 31H9.978l-9.8 15 9.8 15H44z" overflow="visible"/> + <path fill="#fff" stroke="#9ebfea" stroke-width=".75" d="M34.794 33.844L24.504 51.57l-4.03-2.974-3.16 4.215 6.447 4.775 2.357 1.735 1.488-2.542 11.714-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#d1d3d4" d="M37.693 128.84l-12.435 21.73-7.02-5.58 1.646-2.202 4.6 3.656 10.882-19.018"/> + <path fill="#9dbfdf" d="M35.366 158.428l2.327 1.41-12.435 21.734-7.02-5.58 1.648-2.202 4.6 3.657M8.188 166.5v3.438H4.844v3.03h3.343v3.407h2.938v-3.406h3.344v-3.033h-3.346V166.5H8.187z"/> + <path fill="#54595d" d="M18.237 206.99l1.647-2.2 4.6 3.656 10.882-19.018 2.327 1.41-12.435 21.734"/> + <path fill="#9dbfdf" d="M19.884 235.79l4.6 3.656 10.882-19.018 2.327 1.41-12.435 21.734-7.02-5.58"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.png b/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.png Binary files differnew file mode 100644 index 00000000..17de9755 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.png diff --git a/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.svg b/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.svg new file mode 100644 index 00000000..5f15ad22 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.svg @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="44" height="248"> + <path fill="#eaecf0" d="M.177 62H34.2L44 77l-9.8 15H.176z" overflow="visible"/> + <path fill="#3e3e3e" d="M22.338 64.844L12.048 82.57 8.02 79.597 4.86 83.81l6.445 4.774 2.356 1.735 1.488-2.542 11.715-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#c8dbf3" d="M.177 93H34.2l9.8 15-9.8 15H.176z" overflow="visible"/> + <path fill="#36c" stroke="#36c" stroke-width=".75" d="M22.338 95.844l-10.29 17.727-4.028-2.974-3.16 4.215 6.445 4.774 2.356 1.735 1.488-2.542 11.715-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#eaecf0" d="M.177 0H34.2L44 15l-9.8 15H.176z" overflow="visible"/> + <path fill="#fff" stroke="#aeaeae" stroke-width=".75" d="M22.338 2.844L12.048 20.57 8.02 17.597 4.86 21.81l6.445 4.774 2.356 1.735 1.488-2.542 11.715-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#c8dbf3" d="M.177 31H34.2L44 46l-9.8 15H.176z" overflow="visible"/> + <path fill="#fff" stroke="#9ebfea" stroke-width=".75" d="M22.338 33.844L12.048 51.57 8.02 48.597 4.86 52.81l6.445 4.774 2.356 1.735 1.488-2.542 11.715-20.33-4.525-2.604z" overflow="visible"/> + <path fill="#d1d3d4" d="M22.91 127.428l2.328 1.41-12.435 21.734-7.02-5.58 1.646-2.202 4.6 3.656"/> + <path fill="#9dbfdf" d="M12.03 177.447l10.88-19.02 2.328 1.412-12.435 21.732-7.02-5.58 1.647-2.202M35.99 166.5v3.438h3.343v3.03H35.99v3.407h-2.938v-3.406h-3.344v-3.032h3.344V166.5h2.938z"/> + <path fill="#54595d" d="M12.803 212.572l-7.02-5.58 1.646-2.202 4.6 3.656 10.88-19.018 2.328 1.41"/> + <path fill="#9dbfdf" d="M5.782 237.99l1.647-2.2 4.6 3.656 10.88-19.018 2.328 1.41-12.435 21.734"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/close.png b/www/wiki/extensions/Translate/resources/images/close.png Binary files differnew file mode 100644 index 00000000..84631c3d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/close.png diff --git a/www/wiki/extensions/Translate/resources/images/close.svg b/www/wiki/extensions/Translate/resources/images/close.svg new file mode 100644 index 00000000..22619b95 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/close.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"> + <path d="M3.636 2.222l14.142 14.142-1.414 1.414L2.222 3.636z"/> + <path d="M17.778 3.636L3.636 17.778l-1.414-1.414L16.364 2.222z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/contract-ltr.png b/www/wiki/extensions/Translate/resources/images/contract-ltr.png Binary files differnew file mode 100644 index 00000000..11296ee3 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/contract-ltr.png diff --git a/www/wiki/extensions/Translate/resources/images/contract-ltr.svg b/www/wiki/extensions/Translate/resources/images/contract-ltr.svg new file mode 100644 index 00000000..2b7d71b6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/contract-ltr.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12"> + <path fill="#a2a9b1" d="M10.52 1.412h4.293v9.257H10.52z" overflow="visible"/> + <path fill="none" stroke="#222" stroke-width="1.367" d="M.683.862h14.633V11.14H.683z" overflow="visible"/> + <path fill="#222" d="M7.03 2.22L3.688 5.53l-.406.44.408.405 3.25 3.25.843-.844L5.53 6.5h5.782V5.312H5.595l2.25-2.25-.813-.843z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/contract-rtl.png b/www/wiki/extensions/Translate/resources/images/contract-rtl.png Binary files differnew file mode 100644 index 00000000..4c6d9acf --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/contract-rtl.png diff --git a/www/wiki/extensions/Translate/resources/images/contract-rtl.svg b/www/wiki/extensions/Translate/resources/images/contract-rtl.svg new file mode 100644 index 00000000..8a498500 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/contract-rtl.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12"> + <path fill="#a2a9b1" d="M5.48 1.412H1.186v9.257H5.48z" overflow="visible"/> + <path fill="none" stroke="#222" stroke-width="1.367" d="M15.316.862H.683V11.14h14.633z" overflow="visible"/> + <path fill="#222" d="M8.97 2.22l-.845.842 2.28 2.25H4.69V6.5h5.78L8.19 8.78l.843.845 3.25-3.25.408-.406-.407-.44-3.31-3.31z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/edit-mark.png b/www/wiki/extensions/Translate/resources/images/edit-mark.png Binary files differnew file mode 100644 index 00000000..c5e0cd9f --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/edit-mark.png diff --git a/www/wiki/extensions/Translate/resources/images/edit-mark.svg b/www/wiki/extensions/Translate/resources/images/edit-mark.svg new file mode 100644 index 00000000..80d09ecb --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/edit-mark.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="33.389" height="22.857" viewBox="0 0 33.389 22.857"> + <path fill="#c8ccd1" d="M21.325 2.915l-9.343 13.31-.022.004-.284 3.238 2.968-1.38 9.344-13.31-2.663-1.863z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/expand-ltr.png b/www/wiki/extensions/Translate/resources/images/expand-ltr.png Binary files differnew file mode 100644 index 00000000..1d1a8b8c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/expand-ltr.png diff --git a/www/wiki/extensions/Translate/resources/images/expand-ltr.svg b/www/wiki/extensions/Translate/resources/images/expand-ltr.svg new file mode 100644 index 00000000..bffe869c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/expand-ltr.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12"> + <path fill="#a2a9b1" d="M10.52 1.412h4.293v9.257H10.52z" overflow="visible"/> + <path fill="none" stroke="#222" stroke-width="1.367" d="M.683.862h14.633V11.14H.683z" overflow="visible"/> + <path fill="#222" d="M8.406 2.22l-.844.842 2.282 2.25h-5.72V6.5h5.782l-2.28 2.28.843.845 3.25-3.25.405-.406-.406-.44-3.314-3.31z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/expand-rtl.png b/www/wiki/extensions/Translate/resources/images/expand-rtl.png Binary files differnew file mode 100644 index 00000000..00e8f3e6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/expand-rtl.png diff --git a/www/wiki/extensions/Translate/resources/images/expand-rtl.svg b/www/wiki/extensions/Translate/resources/images/expand-rtl.svg new file mode 100644 index 00000000..d65d5660 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/expand-rtl.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12"> + <path fill="#a2a9b1" d="M5.48 1.412H1.186v9.257H5.48z" overflow="visible"/> + <path fill="none" stroke="#222" stroke-width="1.367" d="M15.316.862H.683V11.14h14.633z" overflow="visible"/> + <path fill="#222" d="M7.563 2.22L4.25 5.53l-.406.44.406.405 3.25 3.25.844-.844-2.25-2.28h5.75V5.312H6.156l2.25-2.25-.844-.843z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/label-clock.png b/www/wiki/extensions/Translate/resources/images/label-clock.png Binary files differnew file mode 100644 index 00000000..805dd5ed --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-clock.png diff --git a/www/wiki/extensions/Translate/resources/images/label-clock.svg b/www/wiki/extensions/Translate/resources/images/label-clock.svg new file mode 100644 index 00000000..d1bd36d9 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-clock.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <path fill="#54595d" d="M7.5.125C3.44.125.125 3.44.125 7.5c0 4.06 3.316 7.375 7.375 7.375 4.06 0 7.375-3.316 7.375-7.375C14.875 3.44 11.56.125 7.5.125zm0 1.768c3.104 0 5.607 2.504 5.607 5.607s-2.504 5.607-5.607 5.607S1.893 10.604 1.893 7.5 4.396 1.893 7.5 1.893z"/> + <path fill="#54595d" d="M6.708 2.99v5.763h3.428v-1.21H7.918V2.99h-1.21z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/label-flag.png b/www/wiki/extensions/Translate/resources/images/label-flag.png Binary files differnew file mode 100644 index 00000000..8d5b09d6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-flag.png diff --git a/www/wiki/extensions/Translate/resources/images/label-flag.svg b/www/wiki/extensions/Translate/resources/images/label-flag.svg new file mode 100644 index 00000000..2ba48b7e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-flag.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <path fill="#54595d" d="M2.437 1.75v6.47h8.844v5.03h1.282V1.75H2.437z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/label-page-tick.png b/www/wiki/extensions/Translate/resources/images/label-page-tick.png Binary files differnew file mode 100644 index 00000000..d75a1e7a --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-page-tick.png diff --git a/www/wiki/extensions/Translate/resources/images/label-page-tick.svg b/www/wiki/extensions/Translate/resources/images/label-page-tick.svg new file mode 100644 index 00000000..bdd25395 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-page-tick.svg @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="15" + height="15" + viewBox="0 0 15 15" + id="svg17805" + xml:space="preserve"><metadata + id="metadata12"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs10" /> +<g + transform="translate(-159.64355,-100.30805)" + id="layer1"> + <g + transform="translate(-279.58355,-315.7339)" + id="g18113"> + <path + d="m 440.281,419.25 v 0.562 9.656 0.532 h 0.531 9.657 0.531 v -0.531 -9.656 -0.562 h -0.531 -9.657 -0.531 z m 1.063,1.094 h 8.594 v 8.594 h -8.594 v -8.594 z" + id="rect8974-9-6-80-1" + style="fill:#565656" /> + <path + d="m 447.443,421.423 -2.858,4.925 -1.12,-0.826 -0.878,1.171 1.791,1.326 0.655,0.482 0.413,-0.707 3.254,-5.648 -1.257,-0.723 z" + id="path12436-0-1-2-8-1-9-2-0-5-7" + style="fill:#565656" /> + <path + d="m 442.438,417.094 v 0.531 2.156 0.531 h 0.562 6.938 v 6.969 0.531 h 0.562 2.156 0.531 v -0.531 -9.656 -0.531 H 452.656 443 442.438 z m 1.093,1.062 h 8.563 v 8.594 h -1.062 v -6.969 -0.531 h -0.531 -6.969 v -1.094 z" + id="rect8974-5-0-4-9" + style="fill:#565656" /> + </g> +</g> +</svg>
\ No newline at end of file diff --git a/www/wiki/extensions/Translate/resources/images/label-page.png b/www/wiki/extensions/Translate/resources/images/label-page.png Binary files differnew file mode 100644 index 00000000..69553463 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-page.png diff --git a/www/wiki/extensions/Translate/resources/images/label-page.svg b/www/wiki/extensions/Translate/resources/images/label-page.svg new file mode 100644 index 00000000..6d95932e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-page.svg @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="15" + height="15" + viewBox="0 0 15 15" + id="svg17805" + xml:space="preserve"><metadata + id="metadata10"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs8" /> + +<g + transform="translate(-159.64355,-100.30805)" + id="layer1"> + <g + transform="translate(-279.14083,-255.12502)" + id="g18109"> + + <rect + width="9.6630001" + height="9.6630001" + x="440.36801" + y="359.19199" + id="rect8974-9-6-80-1-7" + style="fill:none;stroke:#565656;stroke-width:1.07369995;stroke-linecap:round" /> + + <path + d="m 442.544,357.012 v 2.156 h 7.5 v 7.5 h 2.156 v -9.656 h -9.656 z" + id="rect8974-5-0-4-9-8" + style="fill:none;stroke:#565656;stroke-width:1.07369995;stroke-linecap:round" /> + </g> +</g> +</svg>
\ No newline at end of file diff --git a/www/wiki/extensions/Translate/resources/images/label-pen.png b/www/wiki/extensions/Translate/resources/images/label-pen.png Binary files differnew file mode 100644 index 00000000..8cd99c1d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-pen.png diff --git a/www/wiki/extensions/Translate/resources/images/label-pen.svg b/www/wiki/extensions/Translate/resources/images/label-pen.svg new file mode 100644 index 00000000..715470d0 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-pen.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <path fill="#54595d" d="M9.825 1.975l-6.218 8.888h-.015l-.19 2.163 1.976-.92 6.218-8.89-1.77-1.243v.002z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/label-tick.png b/www/wiki/extensions/Translate/resources/images/label-tick.png Binary files differnew file mode 100644 index 00000000..e993469e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-tick.png diff --git a/www/wiki/extensions/Translate/resources/images/label-tick.svg b/www/wiki/extensions/Translate/resources/images/label-tick.svg new file mode 100644 index 00000000..44326a0c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/label-tick.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <path fill="#54595d" d="M10.765 1.078l-5.188 8.938-2.03-1.5L1.95 10.64l3.25 2.407 1.188.875.75-1.28 5.906-10.25-2.28-1.314z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/loading.gif b/www/wiki/extensions/Translate/resources/images/loading.gif Binary files differnew file mode 100644 index 00000000..2212db95 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/loading.gif diff --git a/www/wiki/extensions/Translate/resources/images/loading.svg b/www/wiki/extensions/Translate/resources/images/loading.svg new file mode 100644 index 00000000..7aa88cb4 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/loading.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34.031"> + <g color="#000"> + <path fill-opacity=".083" stroke="#54595d" stroke-opacity=".196" stroke-width=".5" d="M16.978.24C7.715.24.245 7.756.245 17.02c0 9.263 7.47 16.778 16.733 16.778 9.262 0 16.777-7.515 16.777-16.778C33.755 7.755 26.24.24 16.978.24zm0 4.15c6.966 0 12.627 5.66 12.627 12.628 0 6.967-5.66 12.583-12.627 12.583-6.967 0-12.584-5.615-12.584-12.582 0-6.967 5.617-12.627 12.584-12.627z" overflow="visible"/> + <path fill="none" stroke="#36c" stroke-width="2.258" d="M31.677 17.004a14.68 14.68 0 0 1-10.88 14.18" stroke-linecap="round" overflow="visible"/> + </g> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/outdated-ltr.png b/www/wiki/extensions/Translate/resources/images/outdated-ltr.png Binary files differnew file mode 100644 index 00000000..64674cee --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/outdated-ltr.png diff --git a/www/wiki/extensions/Translate/resources/images/outdated-ltr.svg b/www/wiki/extensions/Translate/resources/images/outdated-ltr.svg new file mode 100644 index 00000000..cf0fe1dc --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/outdated-ltr.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="33.389" height="22.857" viewBox="0 0 33.389 22.857"> + <path fill="#fef4ba" d="M0 0h25.922l7.467 11.43-7.468 11.428H0V0z"/> + <path fill="#54595d" d="M17.98 4.357c-4.06 0-7.374 3.316-7.374 7.375 0 4.06 3.315 7.375 7.375 7.375 4.06 0 7.376-3.316 7.376-7.375 0-4.06-3.316-7.375-7.375-7.375zm0 1.768c3.105 0 5.608 2.504 5.608 5.607s-2.504 5.607-5.607 5.607c-3.102 0-5.606-2.505-5.606-5.608 0-3.103 2.504-5.607 5.607-5.607z"/> + <path fill="#54595d" d="M17.19 7.223v5.761h3.427v-1.21H18.4V7.223h-1.21z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/outdated-rtl.png b/www/wiki/extensions/Translate/resources/images/outdated-rtl.png Binary files differnew file mode 100644 index 00000000..fedea4e3 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/outdated-rtl.png diff --git a/www/wiki/extensions/Translate/resources/images/outdated-rtl.svg b/www/wiki/extensions/Translate/resources/images/outdated-rtl.svg new file mode 100644 index 00000000..13bcacd8 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/outdated-rtl.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="33.389" height="22.857" viewBox="0 0 33.389 22.857"> + <path fill="#fef4ba" d="M33.39 0H7.466L0 11.43l7.467 11.428H33.39V0z"/> + <path fill="#54595d" d="M15.408 4.357c-4.06 0-7.375 3.316-7.375 7.375 0 4.06 3.315 7.375 7.375 7.375 4.06 0 7.375-3.316 7.375-7.375 0-4.06-3.316-7.375-7.375-7.375zm0 1.768c3.104 0 5.607 2.504 5.607 5.607s-2.504 5.607-5.607 5.607S9.8 14.834 9.8 11.73c0-3.103 2.505-5.607 5.608-5.607z"/> + <path fill="#54595d" d="M14.616 7.223v5.761h3.428v-1.21h-2.218V7.223h-1.21z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/paste.png b/www/wiki/extensions/Translate/resources/images/paste.png Binary files differnew file mode 100644 index 00000000..8b8b61ce --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/paste.png diff --git a/www/wiki/extensions/Translate/resources/images/paste.svg b/www/wiki/extensions/Translate/resources/images/paste.svg new file mode 100644 index 00000000..932c4b22 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/paste.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"> + <path fill="#54595d" d="M29.077 29.012c0 1.06-.86 1.92-1.92 1.92H4.844c-1.06 0-1.92-.86-1.92-1.92V4.042c0-1.06.86-1.92 1.92-1.92h22.313c1.06 0 1.92.86 1.92 1.92v24.97z"/> + <path fill="#f8f9fa" stroke="#54595d" stroke-width=".5" stroke-miterlimit="10" d="M5.368 4.054h21v24.35h-21z"/> + <path fill="#a2a9b1" d="M7.743 8.896h15.625v2.872H7.743zM7.743 15.09h15.625v2.875H7.743zM7.743 21.287h6.125v2.873H7.743z"/> + <path fill="#414042" d="M18.646 2.814V1.068h-5.292v1.746H10.46v2.338H21.54V2.814"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/plus_darkgray.png b/www/wiki/extensions/Translate/resources/images/plus_darkgray.png Binary files differnew file mode 100644 index 00000000..2cca7c17 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/plus_darkgray.png diff --git a/www/wiki/extensions/Translate/resources/images/plus_darkgray.svg b/www/wiki/extensions/Translate/resources/images/plus_darkgray.svg new file mode 100644 index 00000000..5cf4e598 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/plus_darkgray.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 768">
+ <path fill="#54595d" d="M870.5 445.2V322.8H573.2V25.5H450.8v297.3H153.5v122.4h297.3v297.3h122.4V445.2z"/>
+</svg>
diff --git a/www/wiki/extensions/Translate/resources/images/prog-1.png b/www/wiki/extensions/Translate/resources/images/prog-1.png Binary files differnew file mode 100644 index 00000000..8788c993 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/prog-1.png diff --git a/www/wiki/extensions/Translate/resources/images/prog-2.png b/www/wiki/extensions/Translate/resources/images/prog-2.png Binary files differnew file mode 100644 index 00000000..1e8ff84e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/prog-2.png diff --git a/www/wiki/extensions/Translate/resources/images/prog-3.png b/www/wiki/extensions/Translate/resources/images/prog-3.png Binary files differnew file mode 100644 index 00000000..bfba1464 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/prog-3.png diff --git a/www/wiki/extensions/Translate/resources/images/prog-4.png b/www/wiki/extensions/Translate/resources/images/prog-4.png Binary files differnew file mode 100644 index 00000000..132ee756 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/prog-4.png diff --git a/www/wiki/extensions/Translate/resources/images/prog-5.png b/www/wiki/extensions/Translate/resources/images/prog-5.png Binary files differnew file mode 100644 index 00000000..8b86fbb4 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/prog-5.png diff --git a/www/wiki/extensions/Translate/resources/images/project.png b/www/wiki/extensions/Translate/resources/images/project.png Binary files differnew file mode 100644 index 00000000..0d1abc38 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/project.png diff --git a/www/wiki/extensions/Translate/resources/images/project.svg b/www/wiki/extensions/Translate/resources/images/project.svg new file mode 100644 index 00000000..0e7a128b --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/project.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" xml:space="preserve"> + <g transform="translate(-355.875,-530.20145)"> + <rect width="13.336" height="6.4949999" x="373.173" y="532.79199" style="fill:#fff;fill-opacity:0.675;stroke:#c9c9c9;stroke-width:0.2572;stroke-linecap:round"/> + <linearGradient id="a" x1="716.99408" y1="-394.2966" x2="716.99408" y2="-422.53201" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.0287,0,0,-1.0287,-365.6989,126.7675)"> + <stop style="stop-color:#f4f4f4;stop-opacity:1" offset="0"/> + <stop style="stop-color:#dddddd;stop-opacity:1" offset="1"/> + </linearGradient> + <path d="m 356.261,530.587 h 16.295 l 6.267,6.267 h 8.667 v 24.962 h -31.229 v -31.229 z" style="fill:url(#a);stroke:#9d9d9d;stroke-width:0.77149999;stroke-linecap:square"/> + <path d="m 381.718,548.951 c 0.006,5.437 -4.396,9.848 -9.832,9.854 -5.436,0.006 -9.847,-4.396 -9.854,-9.831 0,-0.008 0,-0.016 0,-0.022 -0.006,-5.436 4.396,-9.847 9.832,-9.854 5.436,-0.006 9.847,4.396 9.854,9.832 0,0.006 0,0.014 0,0.021 z" style="fill:#f0f0f0;stroke:#6d6d6d;stroke-width:0.84439999;stroke-linecap:round"/> + <path d="m 372.031,541.108 c -0.825,0.023 -2.344,0.219 -2.344,0.219 0,0 0.25,0.477 0.25,0.688 0,0.211 -0.062,0.778 -0.062,1.031 0,0.253 0.925,0.053 1.094,-0.031 0.169,-0.084 1.281,-1.062 1.281,-1.062 0,0 0.408,-0.612 0.281,-0.781 -0.031,-0.044 -0.225,-0.072 -0.5,-0.064 l 0,0 z m -4.469,0.718 c 0.06,0.656 0.844,1.094 0.844,1.094 l 0.125,-0.438 c 0,10e-4 -0.67,-0.656 -0.969,-0.656 z m -0.781,0.438 c 0,0 -0.406,0.347 -1.062,0.406 -0.247,0.022 -0.745,-0.072 -1.281,-0.156 -1.188,1.37 -1.995,3.062 -2.281,4.938 l 0.219,-0.312 c 0,0 0.77,1.381 1.188,1.5 0.418,0.119 0.892,0.543 1.25,0.781 0.358,0.238 0.591,1.472 0.531,2.188 -0.06,0.716 0.483,0.804 0.781,1.281 0.298,0.477 0.949,3.326 1.188,3.625 0.239,0.299 0.475,-1.253 0.594,-1.969 0.119,-0.716 1.688,-2.034 1.688,-2.75 0,-0.716 -1.798,-1.736 -2.156,-2.094 -0.358,-0.358 -1.477,-0.531 -2.312,-0.531 -0.835,0 -0.847,-1.128 -1.562,-1.188 -0.715,-0.06 0.125,-1 0.125,-1 0,0 1.958,-0.312 2.375,-0.312 0.417,0 0.54,-1.182 0.719,-1.719 0.179,-0.537 1.506,-0.486 1.625,-0.844 0.119,-0.358 -0.542,-0.719 -0.781,-0.719 -0.239,0 -1.259,0.364 -1.438,0.125 -0.179,-0.239 1.031,-0.969 1.031,-0.969 l -0.441,-0.281 z m 9.063,0 c -0.197,0.016 -0.374,0.052 -0.438,0.094 -0.127,0.084 -0.467,0.216 -0.594,0.406 -0.127,0.19 -0.272,0.624 -0.125,0.75 0.147,0.126 0.26,0.157 0.344,0.094 0.084,-0.063 0.729,-0.616 0.812,-0.531 0.083,0.085 0.283,0.594 0.156,0.594 -0.127,0 -1.031,0.281 -1.031,0.281 0,0 -0.003,-0.171 -0.062,-0.156 -0.06,0.015 -0.003,0.235 -0.062,0.25 -0.06,0.015 -0.844,0.375 -0.844,0.375 0,0 -0.1,0.088 -0.219,0.188 l -0.25,0.25 c -0.006,0.007 -0.027,0.025 -0.031,0.031 -0.06,0.104 -0.39,0.599 -0.375,0.719 0.016,0.119 -0.07,0.375 0.094,0.375 0.164,0 1.27,-0.682 1.344,-0.562 0.074,0.119 0.188,0.125 0.188,0.125 0,0 -0.074,-0.156 0,-0.156 0.074,0 0.562,0.188 0.562,0.188 0,0 -0.073,-0.312 0.031,-0.281 0.104,0.03 0.583,0.594 0.688,0.594 0.105,0 0.358,-0.007 0.344,-0.156 -0.015,-0.149 -0.068,-0.335 0.125,-0.469 0.193,-0.134 0.787,-0.231 0.906,-0.156 0.119,0.074 0.304,0.16 0.125,0.25 -0.18,0.089 -0.396,-0.014 -0.5,0.031 -0.104,0.044 -0.392,0.333 -0.406,0.438 -0.015,0.104 0.561,0.565 0.531,0.625 -0.029,0.06 -0.326,0.344 -0.625,0.344 -0.299,0 -2.484,-0.546 -2.812,-0.531 -0.328,0.015 -0.615,0.099 -0.75,0.219 -0.135,0.119 -0.707,0.648 -0.781,0.812 -0.074,0.164 -0.398,0.957 -0.219,1.375 0.179,0.418 0.584,1.176 1.031,1.25 0.447,0.074 0.99,0.107 1.125,0.062 0.135,-0.045 0.859,0.76 0.875,1.312 0.015,0.552 -0.039,1.938 0.125,2.281 0.164,0.343 0.291,0.982 0.812,0.938 0.521,-0.044 0.945,-0.168 1.125,-0.406 0.179,-0.238 0.678,-1.455 0.812,-2.156 0.134,-0.701 0.754,-1.754 0.844,-2.156 0.09,-0.402 0.102,-0.631 -0.062,-0.75 -0.164,-0.119 -0.485,-0.116 -0.5,-0.25 -0.016,-0.135 0.209,-0.016 0.344,0 0.135,0.015 0.631,-0.022 0.75,-0.156 0.119,-0.135 0.324,-0.571 0.25,-0.75 -0.074,-0.18 -0.406,-0.562 -0.406,-0.562 0,0 1.338,0.161 1.875,0.594 0.312,0.251 0.564,0.577 0.75,0.844 -0.047,-2.462 -0.994,-4.705 -2.531,-6.406 -0.014,0.002 -0.054,0.031 -0.062,0.031 -0.232,0 -2.031,0.344 -2.031,0.344 0,0 -0.393,-0.447 -0.688,-0.469 -0.149,-0.016 -0.367,-0.021 -0.564,-0.005 l 0,0 z m -2.282,1.312 c 0,0 -0.212,0.865 0.094,0.844 0.306,-0.021 -0.094,-0.844 -0.094,-0.844 z m -0.406,0.375 c 0,0 -0.365,0.187 -0.281,0.281 0.084,0.094 0.26,0.147 0.312,0.062 0.052,-0.085 -0.031,-0.343 -0.031,-0.343 l 0,0 z m 5.719,8.094 c -0.121,0.026 -0.265,0.124 -0.344,0.219 -0.079,0.095 -0.198,0.43 -0.156,0.625 0.042,0.195 0.218,0.459 0.281,0.438 0.063,-0.021 0.229,-0.084 0.25,-0.438 0.021,-0.354 0.078,-0.671 0.031,-0.75 -0.047,-0.079 -0.062,-0.094 -0.062,-0.094 l 0,0 z" style="fill:#6d6d6d"/> + </g> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/remove.png b/www/wiki/extensions/Translate/resources/images/remove.png Binary files differnew file mode 100644 index 00000000..672e3586 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/remove.png diff --git a/www/wiki/extensions/Translate/resources/images/remove.svg b/www/wiki/extensions/Translate/resources/images/remove.svg new file mode 100644 index 00000000..3cc0c703 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/remove.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"> + <defs> + <linearGradient id="c"> + <stop offset="0" stop-color="#fa9a87"/> + <stop offset="1" stop-color="#e9594d"/> + </linearGradient> + <linearGradient id="b"> + <stop offset="0" stop-color="#fddbd4"/> + <stop offset="1" stop-color="#e47871"/> + </linearGradient> + <linearGradient id="a"> + <stop offset="0" stop-color="#c14d33"/> + <stop offset="1" stop-color="#e0696a"/> + </linearGradient> + <linearGradient id="e" x1="12.758" x2="2.583" y1="13.001" y2="2.521" xlink:href="#a" gradientUnits="userSpaceOnUse"/> + <linearGradient id="d" x1="2.583" x2="12.758" y1="2.521" y2="13.001" xlink:href="#b" gradientUnits="userSpaceOnUse"/> + <linearGradient id="f" x1="4.551" x2="11.459" y1="4.433" y2="11.341" xlink:href="#c" gradientUnits="userSpaceOnUse"/> + </defs> + <path fill="url(#d)" fill-rule="evenodd" stroke="url(#e)" d="M14.647 7.824a6.85 6.85 0 1 1-13.7 0 6.85 6.85 0 1 1 13.7 0z" transform="translate(.253 .127)"/> + <path fill="url(#f)" d="M13.006 7.982a4.988 4.988 0 1 1-9.976 0 4.988 4.988 0 1 1 9.976 0z" transform="translate(-.038) scale(1.002)"/> + <path fill="#fff" fill-opacity=".957" d="M5 9h6V7H5.004z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/search.png b/www/wiki/extensions/Translate/resources/images/search.png Binary files differnew file mode 100644 index 00000000..97e30d24 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/search.png diff --git a/www/wiki/extensions/Translate/resources/images/search.svg b/www/wiki/extensions/Translate/resources/images/search.svg new file mode 100644 index 00000000..e9d42573 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/search.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"> + <path fill="#54595d" d="M3.377 3.377c4.503-4.503 11.806-4.503 16.31 0 4.07 4.072 4.415 10.425 1.122 14.936L32 29.503 29.504 32l-11.19-11.19C13.8 24.102 7.448 23.757 3.376 19.684c-4.503-4.502-4.503-11.805 0-16.308zM5.5 5.5c-3.344 3.34-3.385 8.763-.043 12.105 3.343 3.343 8.806 3.343 12.148 0 3.343-3.342 3.3-8.764-.04-12.106-3.344-3.343-8.724-3.343-12.066 0z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/switch.png b/www/wiki/extensions/Translate/resources/images/switch.png Binary files differnew file mode 100644 index 00000000..01a225ae --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/switch.png diff --git a/www/wiki/extensions/Translate/resources/images/switch.svg b/www/wiki/extensions/Translate/resources/images/switch.svg new file mode 100644 index 00000000..40967827 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/switch.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 768"> + <path fill="#54595d" d="M751.863 131.86l-186.744-1.106H349.324V27.837L128 233.67l221.326 188.127V318.88H565.12v215.793H462.2L650.33 756l204.726-221.327H752.14V318.88l-.278-187.02zm-90.467 90.745v4.427h-4.427v-4.427h4.425z"/> + <path fill="#54595d" d="M655.503 228.447c.062-1.788.05-3.578.046-5.367-.025-1.523-.09-3.04.26-4.532 1.39-4.26 2.03-2.45 9.01-5.744.176 1.704.096 3.43.095 5.144.004 1.708-.003 3.417.003 5.125.006 1.19.008 2.377.024 3.566.014.584.016 1.17.022 1.756-.028 4.033.477.91-8.24 5.074.058-1.59.057-3.18.078-4.77.085-3.247.003-6.496.118-9.742.105-2.014.248-4.026.37-6.04.145-3.94 2.308-3.068 8.245-5.887.213-.1.498.79.733 1.42.733 2.91 1.115 5.89 1.545 8.854.457 3.367.885 6.733 1.15 10.12.152 2.527.183 5.053.587 7.555.26 1.833.743 3.62 1.19 5.414.188.823.355 1.65.407 2.492.288 3.13.21 1.59-8.18 3.726-.477.123.093-.98.134-1.47.066-.776.134-1.55.185-2.327.175-2.656.19-3.618.294-6.34.114-4.458.13-8.92.042-13.378.06-1.338 0-2.536-.664-3.706-.49-.78-1.084-1.406-2.002-1.596-1.142-.125-2.296-.1-3.443-.105-.726-.004-1.08-.068-1.787.196-.342.128-1.292.682-.972.506 2.18-1.2 4.274-2.573 6.55-3.575.537-.236-.858.8-1.246 1.24-.412.468-.765.984-1.147 1.476-1.973 2.593-3.097 5.555-3.672 8.73-.33 2.2-.3 4.426-.313 6.644-.014 2.337.006 4.674.04 7.01-.027 1.143.127 2.262.595 3.304.156.295.303.587.43.894l-8.073 4.495c-.105-.3-.243-.58-.38-.864-.523-1.167-.75-2.414-.668-3.7.014-2.338.01-4.675-.037-7.013-.023-2.277.016-4.557.307-6.817.536-3.302 1.61-6.413 3.59-9.152.234-.33 1.82-2.655 2.22-2.923 3.898-2.61 7.296-5.857 11.956-5.82 1.226 0 2.49-.06 3.675.314 1.014.462 1.74 1.232 2.333 2.17.767 1.33.954 2.665.77 4.197-.172 4.53-.317 9.06-.425 13.59-.05 2.837-.087 5.67-.113 8.508-.005.453.346 1.087-.016 1.36-8.296 6.283-8.284 8.008-8.196 4.66-.02-.802-.1-1.598-.26-2.384-.376-1.835-.88-3.642-1.138-5.5-.442-2.542-.443-5.108-.54-7.68-.216-3.374-.638-6.722-1.096-10.07-.422-2.877-.792-5.777-1.63-8.568-1.09-2.503-1.21.16 7.562-5.02.378-.223-.205.062-.3.447-.038.153-.027.314-.04.472-.172 2.04-.458 4.072-.582 6.117-.173 3.214-.062 6.434-.03 9.652.02 1.564.02 3.13.077 4.692-9.543 5.167-8.17 7.38-8.24 3.323.006-.595.008-1.188.022-1.78.016-1.2.02-2.4.03-3.597.01-1.715.016-3.43.035-5.145.008-1.604.02-3.21-.087-4.81 8.643-4.672 9.406-6.928 7.386-2.63-.568 1.375-.443 2.85-.45 4.312-.003 1.773-.016 3.548.046 5.32l-8.203 4.18z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/translate-ltr.png b/www/wiki/extensions/Translate/resources/images/translate-ltr.png Binary files differnew file mode 100644 index 00000000..d52c25c1 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/translate-ltr.png diff --git a/www/wiki/extensions/Translate/resources/images/translate-ltr.svg b/www/wiki/extensions/Translate/resources/images/translate-ltr.svg new file mode 100644 index 00000000..7bfc8618 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/translate-ltr.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="33.389" height="22.857" viewBox="0 0 33.389 22.857"> + <path fill="#729fcf" d="M0 0h25.922l7.467 11.43-7.468 11.428H0V0z"/> + <path fill="#fff" d="M21.324 2.915l-9.343 13.31-.02.004-.285 3.238 2.968-1.38 9.344-13.31-2.663-1.863z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/translate-rtl.png b/www/wiki/extensions/Translate/resources/images/translate-rtl.png Binary files differnew file mode 100644 index 00000000..31466b2c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/translate-rtl.png diff --git a/www/wiki/extensions/Translate/resources/images/translate-rtl.svg b/www/wiki/extensions/Translate/resources/images/translate-rtl.svg new file mode 100644 index 00000000..9e9857f8 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/translate-rtl.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="33.389" height="22.857" viewBox="0 0 33.389 22.857"> + <path fill="#729fcf" d="M33.39 0H7.466L0 11.43l7.467 11.428H33.39V0z"/> + <path fill="#fff" d="M21.05 2.915l-9.342 13.31-.022.004-.284 3.238 2.968-1.38 9.344-13.31-2.663-1.863z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/trash_darkgray.png b/www/wiki/extensions/Translate/resources/images/trash_darkgray.png Binary files differnew file mode 100644 index 00000000..8e3eaca6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/trash_darkgray.png diff --git a/www/wiki/extensions/Translate/resources/images/trash_darkgray.svg b/www/wiki/extensions/Translate/resources/images/trash_darkgray.svg new file mode 100644 index 00000000..3d1bef76 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/trash_darkgray.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
+ <path fill="#54595d" d="M13.5 28.2l8.1 63c.3 2.7 2.4 4.8 5.1 4.8h42.6c2.7 0 4.8-2.1 4.8-4.5l8.1-63H13.5v-.3zM77.4 6.6C81 6.6 84 8.1 84 9.9v7.2c0 1.8-3 1.5-6.9 1.5H18.6c-3.6 0-6.6.3-6.6-1.5V9.9c0-1.8 3-3.3 6.9-3.3l13.5-1.2L37.2 0h21.3l5.1 5.7 13.8.9z"/>
+</svg>
diff --git a/www/wiki/extensions/Translate/resources/images/user-small.png b/www/wiki/extensions/Translate/resources/images/user-small.png Binary files differnew file mode 100644 index 00000000..f8a4cbe9 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/user-small.png diff --git a/www/wiki/extensions/Translate/resources/images/user-small.svg b/www/wiki/extensions/Translate/resources/images/user-small.svg new file mode 100644 index 00000000..af79fbf1 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/user-small.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"> + <g fill="#c8ccd1"> + <path d="M12.28 4.776c0 2.64-2.14 4.783-4.78 4.783S2.723 7.414 2.723 4.775C2.722 2.138 4.862 0 7.502 0c2.638 0 4.78 2.138 4.78 4.776z"/> + <path d="M13.36 8.61h-.947c-1.03 1.624-2.844 2.706-4.912 2.706-2.067 0-3.882-1.082-4.913-2.707h-.942c-.445 0-.8.357-.8.8v4.795c0 .436.354.795.8.795H13.36c.44 0 .794-.36.794-.795V9.41c0-.442-.353-.8-.794-.8z"/> + </g> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-list-hi.png b/www/wiki/extensions/Translate/resources/images/view-list-hi.png Binary files differnew file mode 100644 index 00000000..4822d7a6 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-list-hi.png diff --git a/www/wiki/extensions/Translate/resources/images/view-list-hi.svg b/www/wiki/extensions/Translate/resources/images/view-list-hi.svg new file mode 100644 index 00000000..e9798819 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-list-hi.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <path fill="#fff" d="M5.954 1.63h13.092V4.3H5.954zM5.954 6.166h13.092v2.668H5.954zM5.954 10.7h13.092v2.67H5.954z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-list.png b/www/wiki/extensions/Translate/resources/images/view-list.png Binary files differnew file mode 100644 index 00000000..21872a06 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-list.png diff --git a/www/wiki/extensions/Translate/resources/images/view-list.svg b/www/wiki/extensions/Translate/resources/images/view-list.svg new file mode 100644 index 00000000..49f03d57 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-list.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <path fill="#222" d="M5.954 1.63h13.092V4.3H5.954zM5.954 6.166h13.092v2.668H5.954zM5.954 10.7h13.092v2.67H5.954z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-page-hi.png b/www/wiki/extensions/Translate/resources/images/view-page-hi.png Binary files differnew file mode 100644 index 00000000..4d0ef7cd --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-page-hi.png diff --git a/www/wiki/extensions/Translate/resources/images/view-page-hi.svg b/www/wiki/extensions/Translate/resources/images/view-page-hi.svg new file mode 100644 index 00000000..24ad3426 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-page-hi.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <path fill="#fff" d="M5.97 1.625v2.688h5.655V1.624H5.97zm7.405 0v2.688h5.656V1.624h-5.655zM5.97 6.156v2.688h5.655V6.156H5.97zm7.405 0v2.688h5.656V6.156h-5.655zM5.97 10.688v2.687h5.655v-2.688H5.97zm7.405 0v2.687h5.656v-2.688h-5.655z"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-page.png b/www/wiki/extensions/Translate/resources/images/view-page.png Binary files differnew file mode 100644 index 00000000..e348f639 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-page.png diff --git a/www/wiki/extensions/Translate/resources/images/view-page.svg b/www/wiki/extensions/Translate/resources/images/view-page.svg new file mode 100644 index 00000000..7e983678 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-page.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <g fill="#222"> + <path d="M5.97 1.625v2.688h5.655V1.624H5.97zm7.405 0v2.688h5.656V1.624h-5.655zM5.97 6.156v2.688h5.655V6.156H5.97zm7.405 0v2.688h5.656V6.156h-5.655zM5.97 10.688v2.687h5.655v-2.688H5.97zm7.405 0v2.687h5.656v-2.688h-5.655z"/> + </g> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-proofread-hi.png b/www/wiki/extensions/Translate/resources/images/view-proofread-hi.png Binary files differnew file mode 100644 index 00000000..78650b79 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-proofread-hi.png diff --git a/www/wiki/extensions/Translate/resources/images/view-proofread-hi.svg b/www/wiki/extensions/Translate/resources/images/view-proofread-hi.svg new file mode 100644 index 00000000..4d38ee33 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-proofread-hi.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <path fill="#fff" d="M15.766 1.078l-5.188 8.938-2.03-1.5-1.595 2.125 3.25 2.407 1.188.875.75-1.28 5.907-10.25-2.28-1.314z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/images/view-proofread.png b/www/wiki/extensions/Translate/resources/images/view-proofread.png Binary files differnew file mode 100644 index 00000000..eabdbab7 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-proofread.png diff --git a/www/wiki/extensions/Translate/resources/images/view-proofread.svg b/www/wiki/extensions/Translate/resources/images/view-proofread.svg new file mode 100644 index 00000000..1b3f287b --- /dev/null +++ b/www/wiki/extensions/Translate/resources/images/view-proofread.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="25" height="15" viewBox="0 0 25 15"> + <path fill="#222" d="M15.766 1.078l-5.188 8.938-2.03-1.5-1.595 2.125 3.25 2.407 1.188.875.75-1.28 5.907-10.25-2.28-1.314z" overflow="visible"/> +</svg> diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.base.js b/www/wiki/extensions/Translate/resources/js/ext.translate.base.js new file mode 100644 index 00000000..36ffeed2 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.base.js @@ -0,0 +1,192 @@ +( function () { + 'use strict'; + + mw.translate = mw.translate || {}; + + mw.translate = $.extend( mw.translate, { + dirty: false, + // A cache for language stats loaded from API, + // indexed by language code + languagestats: {}, + + /** + * Checks if the input placeholder attribute + * is supported on this element in this browser. + * + * @param {jQuery} $element + * @return {boolean} + */ + isPlaceholderSupported: function ( $element ) { + return ( 'placeholder' in $element[ 0 ] ); + }, + + // Storage for language stats loader functions from API, + // indexed by language code + languageStatsLoader: {}, + + /** + * Get language stats for a language from the API. + * + * @param {string} language Language code. + * @return {deferred} + */ + loadLanguageStats: function ( language ) { + if ( !mw.translate.languageStatsLoader[ language ] ) { + mw.translate.languageStatsLoader[ language ] = new mw.Api().get( { + action: 'query', + meta: 'languagestats', + lslanguage: language + } ); + } + + mw.translate.languageStatsLoader[ language ].done( function ( result ) { + mw.translate.languagestats[ language ] = result.query.languagestats; + } ); + + return mw.translate.languageStatsLoader[ language ]; + }, + + /** + * Load message group information asynchronously. + * + * @param {string} id Message group id + * @param {string|Array} [props] List of properties to load + * @return {jQuery.Promise} Object containing the requested properties on success. + */ + getMessageGroup: function ( id, props ) { + var params, api; + + if ( Array.isArray( props ) ) { + props = props.join( '|' ); + } else if ( props === undefined ) { + props = 'id|label|description|icon|priority|prioritylangs|priorityforce|workflowstates'; + } + + params = { + meta: 'messagegroups', + mgformat: 'flat', + mgprop: props, + mgroot: id, + formatversion: 2 + }; + + api = new mw.Api(); + + return api.get( params ).then( function ( result ) { + return result.query.messagegroups[ 0 ]; + } ); + }, + + /** + * Find a group from an array of message groups as returned by web api + * and recurse it through sub groups. + * + * @param {string} id Group id to search for. + * @param {Array} groups Array of message grous + * @return {Object} Message group object + */ + findGroup: function ( id, groups ) { + var result = null; + + if ( !id ) { + return groups; + } + + $.each( groups, function ( index, group ) { + if ( group.id === id ) { + result = group; + return false; + } + + if ( group.groups ) { + group = mw.translate.findGroup( id, group.groups ); + + if ( group ) { + result = group; + return false; + } + } + } ); + + return result; + }, + + /** + * Check if the current user is allowed to translate on this wiki. + * + * @return {boolean} + */ + canTranslate: function () { + return mw.config.get( 'TranslateRight' ); + }, + + /** + * Check if the current user is allowed to proofread on this wiki. + * + * @return {boolean} + */ + canProofread: function () { + return mw.config.get( 'TranslateMessageReviewRight' ); + }, + + /** + * Check if the current user can delete translations on this wiki. + * + * @return {boolean} + */ + canDelete: function () { + return mw.config.get( 'DeleteRight' ) && mw.config.get( 'TranslateRight' ); + }, + + /** + * Adds missing languages to the language database so that they can be used in ULS. + * + * @param {Object} languages Language tags mapped to language names + * @param {Array} regions Which regions to add the languages. + */ + addExtraLanguagesToLanguageData: function ( languages, regions ) { + var code; + for ( code in languages ) { + if ( code in $.uls.data.languages ) { + continue; + } + + $.uls.data.addLanguage( code, { + script: 'Zyyy', + regions: regions, + autonym: languages[ code ] + } ); + } + }, + + isDirty: function () { + return $( '.mw-ajax-dialog:visible' ).length || // For old Translate + // For new Translate, something being typed in the current editor. + mw.translate.dirty || + // For new translate, previous editors has some unsaved edits + $( '.tux-status-unsaved' ).length; + } + } ); + + function pageShowHandler() { + $( window ).on( 'beforeunload.translate', function () { + if ( mw.translate.isDirty() ) { + // Return our message + return mw.msg( 'translate-js-support-unsaved-warning' ); + } + } ); + } + + /** + * A warning to be shown if a user tries to close the page or navigate away + * from it without saving the written translation. + */ + function translateOnBeforeUnloadRegister() { + pageShowHandler(); + $( window ).on( 'pageshow.translate', pageShowHandler ); + } + + $( function () { + translateOnBeforeUnloadRegister(); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.dropdownmenu.js b/www/wiki/extensions/Translate/resources/js/ext.translate.dropdownmenu.js new file mode 100644 index 00000000..a3333801 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.dropdownmenu.js @@ -0,0 +1,12 @@ +( function () { + 'use strict'; + + $( function () { + // Hide the dropdown menu when clicking outside of it + $( 'html' ).on( 'click', function ( e ) { + if ( !e.isDefaultPrevented() ) { + $( '.tux-dropdown-menu' ).addClass( 'hide' ); + } + } ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.editor.helpers.js b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.helpers.js new file mode 100644 index 00000000..38cb9a46 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.helpers.js @@ -0,0 +1,542 @@ +/*! + * Translate editor additional helper functionality + */ +( function () { + 'use strict'; + + var translateEditorHelpers = { + + showDocumentationEditor: function () { + var $infoColumnBlock = this.$editor.find( '.infocolumn-block' ), + $editColumn = this.$editor.find( '.editcolumn' ), + $messageDescEditor = $infoColumnBlock.find( '.message-desc-editor' ), + $messageDescViewer = $infoColumnBlock.find( '.message-desc-viewer' ); + + $infoColumnBlock + .removeClass( 'five' ) + .addClass( 'seven' ); + $editColumn + .removeClass( 'seven' ) + .addClass( 'five' ); + + $messageDescViewer.addClass( 'hide' ); + + $messageDescEditor.removeClass( 'hide' ); + $messageDescEditor.find( '.tux-textarea-documentation' ).focus(); + + // So that the link won't be followed + return false; + }, + + hideDocumentationEditor: function () { + var $infoColumnBlock = this.$editor.find( '.infocolumn-block' ), + $editColumn = this.$editor.find( '.editcolumn' ), + $messageDescEditor = $infoColumnBlock.find( '.message-desc-editor' ), + $messageDescViewer = $infoColumnBlock.find( '.message-desc-viewer' ); + + $infoColumnBlock + .removeClass( 'seven' ) + .addClass( 'five' ); + $editColumn + .removeClass( 'five' ) + .addClass( 'seven' ); + + $messageDescEditor.addClass( 'hide' ); + $messageDescViewer.removeClass( 'hide' ); + }, + + /** + * Save the documentation + * + * @return {jQuery.Promise} + */ + saveDocumentation: function () { + var translateEditor = this, + api = new mw.Api(), + newDocumentation = translateEditor.$editor.find( '.tux-textarea-documentation' ).val(); + + return api.postWithToken( 'csrf', { + action: 'edit', + title: translateEditor.message.title + .replace( /\/[a-z-]+$/, '/' + mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ), + text: newDocumentation + } ).done( function ( response ) { + var $messageDesc = translateEditor.$editor.find( '.infocolumn-block .message-desc' ); + + if ( response.edit.result === 'Success' ) { + api.parse( + newDocumentation + ).done( function ( parsedDocumentation ) { + $messageDesc.html( parsedDocumentation ); + } ).fail( function ( errorCode, results ) { + $messageDesc.html( newDocumentation ); + mw.log( 'Error parsing documentation ' + errorCode + ' ' + results.error.info ); + } ); + // A collapsible element may have been added + $( '.mw-identical-title' ).makeCollapsible(); + + translateEditor.hideDocumentationEditor(); + } else { + mw.notify( 'Error saving message documentation' ); + mw.log( 'Error saving documentation', response ); + } + } ).fail( function ( errorCode, results ) { + mw.notify( 'Error saving message documentation' ); + mw.log( 'Error saving documentation', errorCode, results ); + } ); + }, + + /** + * Shows the message documentation. + * + * @param {Object} documentation A documentation object as returned by API. + */ + showMessageDocumentation: function ( documentation ) { + var $descEditLink, + documentationDir, + expand, + $messageDescViewer, + $messageDoc, + readMore, + langAttr, + $readMore = null; + + if ( !mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + return; + } + + $messageDescViewer = this.$editor.find( '.message-desc-viewer' ); + $descEditLink = $messageDescViewer.find( '.message-desc-edit' ); + $messageDoc = $messageDescViewer.find( '.message-desc' ); + + // Display the documentation only if it's not empty and + // documentation language is configured + if ( documentation.error ) { + // TODO: better error handling, especially since the presence of documentation + // is heavily hinted at in the UI + return; + } else if ( documentation.value ) { + documentationDir = $.uls.data.getDir( documentation.language ); + + // Show the documentation and set appropriate + // lang and dir attributes. + // The message documentation is assumed to be written + // in the content language of the wiki. + langAttr = { + lang: documentation.language, + dir: documentationDir + }; + + // Possible classes: + // * mw-content-ltr + // * mw-content-rtl + // (The direction classes are needed, because the documentation + // is likely to be MediaWiki-formatted text.) + $messageDoc + .attr( langAttr ) + .addClass( 'mw-content-' + documentationDir ) + .html( documentation.html ); + + $messageDoc.find( 'a[href]' ).prop( 'target', '_blank' ); + + this.$editor.find( '.tux-textarea-documentation' ) + .attr( langAttr ) + .val( documentation.value ); + + $descEditLink.text( mw.msg( 'tux-editor-edit-desc' ) ); + + if ( documentation.html.length > 500 ) { + expand = function () { + $messageDoc.removeClass( 'compact' ); + $readMore.text( mw.msg( 'tux-editor-message-desc-less' ) ); + }; + + readMore = function () { + if ( $messageDoc.hasClass( 'compact' ) ) { + expand(); + } else { + $messageDoc.addClass( 'compact' ); + $readMore.text( mw.msg( 'tux-editor-message-desc-more' ) ); + } + }; + + $readMore = $( '<span>' ) + .addClass( 'read-more column' ) + .text( mw.msg( 'tux-editor-message-desc-more' ) ) + .click( readMore ); + + $messageDescViewer.find( '.message-desc-control' ) + .prepend( $readMore ); + + $messageDoc.addClass( 'long compact' ).on( 'mouseenter mouseleave', expand ); + } + + // Enable the collapsible elements, + // used in {{Identical}} on translatewiki.net + $( '.mw-identical-title' ).makeCollapsible(); + } else { + $descEditLink.text( mw.msg( 'tux-editor-add-desc' ) ); + } + + $messageDescViewer.removeClass( 'hide' ); + }, + + /** + * Shows uneditable documentation. + * + * @param {Object} documentation A gettext object as returned by API. + */ + showUneditableDocumentation: function ( documentation ) { + var dir; + + if ( documentation.error ) { + return; + } + + dir = $.uls.data.getDir( documentation.language ); + + this.$editor.find( '.uneditable-documentation' ) + .attr( { + lang: documentation.language, + dir: dir + } ) + .addClass( 'mw-content-' + dir ) + .html( documentation.html ) + .removeClass( 'hide' ); + }, + + /** + * Shows the translations from other languages + * + * @param {Array} translations An inotherlanguages array as returned by the translation helpers API. + */ + showAssistantLanguages: function ( translations ) { + var translateEditor = this; + + if ( translations.error ) { + // Do not proceed if errored/unsupported + return; + } + + $.each( translations, function ( index ) { + var $otherLanguage, langAttr, + translation = translations[ index ]; + + langAttr = { + lang: translation.language, + dir: $.uls.data.getDir( translation.language ) + }; + + $otherLanguage = $( '<div>' ) + .addClass( 'row in-other-language' ) + .append( + $( '<div>' ) + .addClass( 'nine columns suggestiontext' ) + .attr( langAttr ) + .text( translation.value ), + $( '<div>' ) + .addClass( 'three columns language text-right' ) + .attr( langAttr ) + .text( $.uls.data.getAutonym( translation.language ) ) + ); + + translateEditor.suggestionAdder( $otherLanguage, translation.value ); + + translateEditor.$editor.find( '.in-other-languages-title' ) + .removeClass( 'hide' ) + .after( $otherLanguage ); + } ); + }, + + /** + * Shows the translation suggestions from Translation Memory + * + * @param {Array} suggestions A ttmserver array as returned by API. + */ + showTranslationMemory: function ( suggestions ) { + var $heading, $tmSuggestions, $messageList, translationLang, translationDir, + translateEditor = this; + + if ( !suggestions.length ) { + return; + } + + // Container for the suggestions + $tmSuggestions = $( '<div>' ).addClass( 'tm-suggestions' ); + + $heading = this.$editor.find( '.tm-suggestions-title' ); + $heading.after( $tmSuggestions ); + + $messageList = $( '.tux-messagelist' ); + translationLang = $messageList.data( 'targetlangcode' ); + translationDir = $messageList.data( 'targetlangdir' ); + + $.each( suggestions, function ( index, translation ) { + var $translation, + alreadyOnTheList = false; + + if ( translation.local && translation.location === translateEditor.message.title ) { + // Do not add self-suggestions + return true; + } + + // See if it is already listed, and increment use count + $tmSuggestions.find( '.tm-suggestion' ).each( function () { + var $uses, count, + $suggestion = $( this ); + + if ( $suggestion.find( '.suggestiontext ' ).text() === translation.target ) { + // Update the message and data value + $uses = $suggestion.find( '.n-uses' ); + count = $uses.data( 'n' ) + 1; + $uses.data( 'n', count ); + $uses.text( mw.msg( 'tux-editor-n-uses', count ) + ' 〉' ); + + // Halt processing + alreadyOnTheList = true; + return false; + } + } ); + + if ( alreadyOnTheList ) { + // Continue to the next one + return true; + } + + $translation = $( '<div>' ) + .addClass( 'row tm-suggestion' ) + .append( + $( '<div>' ) + .addClass( 'nine columns suggestiontext' ) + .attr( { + lang: translationLang, + dir: translationDir + } ) + .text( translation.target ), + $( '<div>' ) + .addClass( 'three columns quality text-right' ) + .text( mw.msg( 'tux-editor-tm-match', + mw.language.convertNumber( Math.floor( translation.quality * 100 ) ) ) ), + $( '<div>' ) + .addClass( 'row text-right' ) + .append( + $( '<a>' ) + .addClass( 'n-uses' ) + .data( 'n', 1 ) + ) + ); + + translateEditor.suggestionAdder( $translation, translation.target ); + + $tmSuggestions.append( $translation ); + } ); + + // Show the heading only if we actually have suggestions + if ( $tmSuggestions.length ) { + $heading.removeClass( 'hide' ); + } + }, + + /** + * Shows the translation from machine translation systems + * + * @param {Array} suggestions + */ + showMachineTranslations: function ( suggestions ) { + var $mtSuggestions, $messageList, translationLang, translationDir, + translateEditor = this; + + if ( !suggestions.length ) { + return; + } + + $mtSuggestions = this.$editor.find( '.tm-suggestions' ); + + if ( !$mtSuggestions.length ) { + $mtSuggestions = $( '<div>' ).addClass( 'tm-suggestions' ); + } + + this.$editor.find( '.tm-suggestions-title' ) + .removeClass( 'hide' ) + .after( $mtSuggestions ); + + $messageList = $( '.tux-messagelist' ); + translationLang = $messageList.data( 'targetlangcode' ); + translationDir = $messageList.data( 'targetlangdir' ); + + $.each( suggestions, function ( index, translation ) { + var $translation; + + $translation = $( '<div>' ) + .addClass( 'row tm-suggestion' ) + .append( + $( '<div>' ) + .addClass( 'nine columns suggestiontext' ) + .attr( { + lang: translationLang, + dir: translationDir + } ) + .text( translation.target ), + $( '<div>' ) + .addClass( 'three columns text-right service' ) + .text( translation.service ) + ); + + translateEditor.suggestionAdder( $translation, translation.target ); + + $mtSuggestions.append( $translation ); + } ); + }, + + /** + * Makes the $source element clickable and clicking it will replace the + * translation textarea with the given suggestion. + * + * @param {jQuery} $source + * @param {string} suggestion Text to add + */ + suggestionAdder: function ( $source, suggestion ) { + var inserter, + $target = this.$editor.find( '.tux-textarea-translation' ); + + inserter = function () { + var selection; + if ( window.getSelection ) { + selection = window.getSelection().toString(); + } else if ( document.selection && document.selection.type !== 'Control' ) { + selection = document.selection.createRange().text; + } + + if ( !selection ) { + $target.val( suggestion ).focus().trigger( 'input' ); + } + }; + + $source.on( 'click', inserter ); + $source.addClass( 'shortcut-activated' ); + }, + + /** + * Shows the support options for the translator. + * + * @param {Object} support A support object as returned by API. + */ + showSupportOptions: function ( support ) { + // Support URL + if ( support.url ) { + this.$editor.find( '.help a' ).attr( 'href', support.url ); + this.$editor.find( '.help' ).removeClass( 'hide' ); + } + }, + + /** + * Adds buttons for quickly inserting insertables. + * + * @param {Object} insertables A insertables object as returned by API. + */ + addInsertables: function ( insertables ) { + var i, + count = insertables.length, + $sourceMessage = this.$editor.find( '.sourcemessage' ), + $buttonArea = this.$editor.find( '.tux-editor-insert-buttons' ), + $textarea = this.$editor.find( '.tux-textarea-translation' ); + + for ( i = 0; i < count; i++ ) { + // The dir and lang attributes must be set here, + // because the language of the insertables is the language + // of the source message and not of the translation. + // The direction may appear confusing, for example, + // in tvar strings, which would appear with the dollar sign + // on the wrong end. + $( '<button>' ) + .prop( { + lang: $sourceMessage.prop( 'lang' ), + dir: $sourceMessage.prop( 'dir' ) + } ) + .addClass( 'insertable shortcut-activated' ) + .text( insertables[ i ].display ) + .data( 'iid', i ) + .appendTo( $buttonArea ); + } + + $buttonArea.on( 'click', '.insertable', function () { + var data = insertables[ $( this ).data( 'iid' ) ]; + $textarea.textSelection( 'encapsulateSelection', { + pre: data.pre, + post: data.post + } ); + $textarea.focus().trigger( 'input' ); + } ); + + this.resizeInsertables( $textarea ); + }, + + /** + * Loads and shows the translation helpers. + */ + showTranslationHelpers: function () { + // API call to get translation suggestions from other languages + // callback should render suggestions to the editor's info column + var translateEditor = this, + api = new mw.Api(); + + api.get( { + action: 'translationaids', + title: this.message.title + } ).done( function ( result ) { + translateEditor.$editor.find( '.infocolumn .loading' ).remove(); + + if ( !result.helpers ) { + mw.log( 'API did not return any translation helpers.' ); + return false; + } + + translateEditor.showMessageDocumentation( result.helpers.documentation ); + translateEditor.showUneditableDocumentation( result.helpers.gettext ); + translateEditor.showAssistantLanguages( result.helpers.inotherlanguages ); + translateEditor.showTranslationMemory( result.helpers.ttmserver ); + translateEditor.showMachineTranslations( result.helpers.mt ); + translateEditor.showSupportOptions( result.helpers.support ); + translateEditor.addDefinitionDiff( result.helpers.definitiondiff ); + translateEditor.addInsertables( result.helpers.insertables ); + + // Load the possible warnings as soon as possible, do not wait + // for the user to make changes. Otherwise users might try confirming + // translations which fail checks. Confirmation seems to work but + // the message will continue to appear outdated. + if ( translateEditor.message.properties && + translateEditor.message.properties.status === 'fuzzy' + ) { + translateEditor.validateTranslation(); + } + + mw.hook( 'mw.translate.editor.showTranslationHelpers' ).fire( result.helpers, translateEditor.$editor ); + + } ).fail( function ( errorCode, results ) { + mw.log( 'Error loading translation aids', errorCode, results ); + } ); + } + }; + + mw.translate = mw.translate || {}; + + mw.translate = $.extend( mw.translate, { + /** + * Get the documentation edit URL for a title + * + * @param {string} title Message title with namespace + * @return {string} URL for editing the documentation + */ + getDocumentationEditURL: function ( title ) { + return mw.util.getUrl( + title + '/' + mw.config.get( 'wgTranslateDocumentationLanguageCode' ), + { action: 'edit' } + ); + } + } ); + + // Extend the translate editor + mw.translate.editor = mw.translate.editor || {}; + $.extend( mw.translate.editor, translateEditorHelpers ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js new file mode 100644 index 00000000..b5e3637d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js @@ -0,0 +1,1324 @@ +/* global autosize */ + +( function () { + 'use strict'; + + /** + * TranslateEditor Plugin + * Prepare the translation editor UI for a translation unit (message). + * This is mainly used with the messagetable plugin, + * but it is independent of messagetable. + * Example usage: + * + * $( 'div.messageRow' ).translateeditor( { + * message: messageObject // Mandatory message object + * } ); + * + * Assumptions: The jquery element to which translateeditor is applied will + * internally contain the editor's generated UI. So it is going to have the same width + * and inherited properies of the container. + * The container can mark the message item with class 'message'. This is not + * mandatory, but if found, when the editor is opened, the message item will be hidden + * and the editor will appear as if the message is replaced by the editor. + * See the UI of Translate messagetable for a demo. + * + * @param {HTMLElement} element + * @param {Object} options + * @param {Function} [options.beforeSave] Callback to call when translation is going to be saved. + * @param {Function} [options.onReady] Callback to call when the editor is ready. + * @param {Function} [options.onSave] Callback to call when translation has been saved. + * @param {Function} [options.onSkip] Callback to call when a message is skipped. + * @param {Object} options.message Object as returned by messagecollection api. + * @param {TranslationApiStorage} [options.storage] + */ + function TranslateEditor( element, options ) { + this.$editTrigger = $( element ); + this.$editor = null; + this.options = options; + this.message = this.options.message; + this.$messageItem = this.$editTrigger.find( '.message' ); + this.shown = false; + this.dirty = false; + this.saving = false; + this.expanded = false; + this.listen(); + this.storage = this.options.storage || new mw.translate.TranslationApiStorage(); + this.canDelete = mw.translate.canDelete(); + this.delayValidation = delayer(); + } + + TranslateEditor.prototype = { + + /** + * Initialize the plugin + */ + init: function () { + // In case we have already created the editor earlier, + // don't add a new one. The existing one may have unsaved + // changes. + if ( this.$editor ) { + return; + } + + this.render(); + // onReady callback + if ( this.options.onReady ) { + this.options.onReady.call( this ); + } + }, + + /** + * Render the editor UI + */ + render: function () { + this.$editor = $( '<div>' ) + .addClass( 'row tux-message-editor hide' ) + .append( + this.prepareEditorColumn(), + this.prepareInfoColumn() + ); + + this.expanded = false; + this.$editTrigger.append( this.$editor ); + + if ( this.message.properties && this.message.properties.status === 'fuzzy' ) { + this.addWarning( + mw.message( 'tux-editor-outdated-warning' ).escaped(), + 'fuzzy' + ); + } + + this.showTranslationHelpers(); + }, + + /** + * Mark the message as unsaved because of edits, can be resumed later + * + * @param {string} [highlightClass] Class for background highlighting + */ + markUnsaved: function ( highlightClass ) { + var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' ); + + highlightClass = highlightClass || 'tux-highlight'; + + $tuxListStatus.children( '.tux-status-unsaved' ).remove(); + $tuxListStatus.children().addClass( 'hide' ); + $( '<span>' ) + .addClass( 'tux-status-unsaved ' + highlightClass ) + .text( mw.msg( 'tux-status-unsaved' ) ) + .appendTo( $tuxListStatus ); + }, + + /** + * Mark the message as unsaved because of saving failure. + */ + markUnsavedFailure: function () { + this.markUnsaved( 'tux-warning' ); + }, + + /** + * Mark the message as no longer unsaved + */ + markUnunsaved: function () { + var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' ); + + $tuxListStatus.children( '.tux-status-unsaved' ).remove(); + $tuxListStatus.children().removeClass( 'hide' ); + + this.dirty = false; + mw.translate.dirty = false; + }, + + /** + * Mark the message as being saved + */ + markSaving: function () { + var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' ); + + // Disable the save button + this.$editor.find( '.tux-editor-save-button' ) + .prop( 'disabled', true ); + + // Add a "Saving" indicator + $tuxListStatus.empty(); + $( '<span>' ) + .addClass( 'tux-status-unsaved' ) + .text( mw.msg( 'tux-status-saving' ) ) + .appendTo( $tuxListStatus ); + }, + + /** + * Mark the message as translated and successfully saved. + */ + markTranslated: function () { + this.$editTrigger.find( '.tux-list-status' ) + .empty() + .append( $( '<span>' ) + .addClass( 'tux-status-translated' ) + .text( mw.msg( 'tux-status-translated' ) ) + ); + + this.$messageItem + .removeClass( 'untranslated translated fuzzy proofread' ) + .addClass( 'translated' ); + + this.dirty = false; + + if ( this.message.properties ) { + $( '.tux-action-bar .tux-statsbar' ).trigger( + 'change', + [ 'translated', this.message.properties.status ] + ); + + this.message.properties.status = 'translated'; + // TODO: Update any other statsbar for the same group in the page. + } + }, + + /** + * Save the translation + */ + save: function () { + var translation, editSummary, + translateEditor = this; + + mw.hook( 'mw.translate.editor.beforeSubmit' ).fire( translateEditor.$editor ); + translation = translateEditor.$editor.find( '.editcolumn textarea' ).val(); + editSummary = translateEditor.$editor.find( '.tux-input-editsummary' ).val() || ''; + + translateEditor.saving = true; + + // beforeSave callback + if ( translateEditor.options.beforeSave ) { + translateEditor.options.beforeSave( translation ); + } + + // For responsiveness and efficiency, + // immediately move to the next message. + translateEditor.next(); + + // Now the message definitely has a history, + // so make sure the history menu item is shown + translateEditor.$editor.find( '.message-tools-history' ) + .removeClass( 'hide' ); + + // Show the delete menu item if the user can delete + if ( this.canDelete ) { + translateEditor.$editor.find( '.message-tools-delete' ) + .removeClass( 'hide' ); + } + + this.storage.save( + translateEditor.message.title, + translation, + editSummary + ).done( function ( response, xhr ) { + var editResp = response.edit; + if ( editResp.result === 'Success' ) { + translateEditor.message.translation = translation; + translateEditor.onSaveSuccess(); + // Handle errors + } else if ( editResp.spamblacklist ) { + // @todo Show exactly which blacklisted URL triggered it + translateEditor.onSaveFail( mw.msg( 'spamprotectiontext' ) ); + } else if ( editResp.info && + editResp.info.indexOf( 'Hit AbuseFilter:' ) === 0 && + editResp.warning + ) { + translateEditor.onSaveFail( editResp.warning ); + } else { + translateEditor.onSaveFail( mw.msg( 'tux-save-unknown-error' ) ); + mw.log( response, xhr ); + } + } ).fail( function ( errorCode, response ) { + translateEditor.onSaveFail( + response.error && response.error.info || mw.msg( 'tux-save-unknown-error' ) + ); + if ( errorCode === 'assertuserfailed' ) { + // eslint-disable-next-line no-alert + alert( mw.msg( 'tux-session-expired' ) ); + } + } ); + }, + + /** + * Success handler for the translation saving. + */ + onSaveSuccess: function () { + this.markTranslated(); + this.$editTrigger.find( '.tux-list-translation' ) + .text( this.message.translation ); + this.saving = false; + + // remove warnings if any. + this.removeWarning( 'diff' ); + this.removeWarning( 'fuzzy' ); + this.removeWarning( 'validation' ); + + this.$editor.find( '.tux-warning' ).empty(); + this.$editor.find( '.tux-more-warnings' ) + .addClass( 'hide' ) + .empty(); + + $( '.tux-editor-clear-translated' ) + .removeClass( 'hide' ) + .prop( 'disabled', false ); + + this.$editor.find( '.tux-input-editsummary' ) + .val( '' ) + .prop( 'disabled', true ); + + // Save callback + if ( this.options.onSave ) { + this.options.onSave( this.message.translation ); + } + + mw.translate.dirty = false; + mw.hook( 'mw.translate.editor.afterSubmit' ).fire( this.$editor ); + + if ( mw.track ) { + mw.track( 'ext.translate.event.translation', this.message ); + } + }, + + /** + * Marks that there was a problem saving a translation. + * + * @param {string} error Strings of warnings to display. + */ + onSaveFail: function ( error ) { + this.addWarning( + mw.msg( 'tux-editor-save-failed', error ), + 'translation-saving' + ); + this.saving = false; + this.markUnsavedFailure(); + }, + + /** + * Skip the current message. + * Record it to mark as hard. + */ + skip: function () { + // @TODO devise good algorithm for identifying hard to translate messages + }, + + /** + * Jump to the next translation editor row. + */ + next: function () { + var $next = this.$editTrigger.next( '.tux-message' ); + + // Skip if the message is hidden. For example in a filter result. + if ( $next.length && $next.hasClass( 'hide' ) ) { + this.$editTrigger = $next; + this.next(); + + return; + } + + // If this is the last message, just hide it + if ( !$next.length ) { + this.hide(); + + return; + } + + $next.data( 'translateeditor' ).show(); + + // Scroll the page a little bit up, slowly. + if ( $( document ).height() - + ( $( window ).height() + window.pageYOffset + $next.height() ) > 0 + ) { + $( 'html, body' ).stop().animate( { + scrollTop: $( '.tux-message-editor:visible' ).offset().top - 85 + }, 500 ); + } + }, + + /** + * Creates a menu element for the message tools. + * + * @param {string} className Used as the element's CSS class + * @param {Object} query Used as the query in the mw.Uri object + * @param {string} message The message of the label of the menu item + * @return {jQuery} The new menu item element + */ + createMessageToolsItem: function ( className, query, message ) { + var uri = new mw.Uri(); + + uri.path = mw.config.get( 'wgScript' ); + uri.query = query; + + return $( '<li>' ) + .addClass( className ) + .append( $( '<a>' ) + .attr( { + href: uri.toString(), + target: '_blank' + } ) + .text( mw.msg( message ) ) + ); + }, + + /** + * Creates an element with a dropdown menu including + * tools for the translators. + * + * @return {jQuery} The new message tools menu element + */ + createMessageTools: function () { + var $editItem, $historyItem, $deleteItem, $translationsItem, $linkToThisItem; + + $editItem = this.createMessageToolsItem( + 'message-tools-edit', + { + title: this.message.title, + action: 'edit' + }, + 'tux-editor-message-tools-show-editor' + ); + + if ( !mw.translate.canTranslate() ) { + $editItem.addClass( 'hide' ); + } + + $historyItem = this.createMessageToolsItem( + 'message-tools-history', + { + title: this.message.title, + action: 'history' + }, + 'tux-editor-message-tools-history' + ); + + $deleteItem = this.createMessageToolsItem( + 'message-tools-delete', + { + title: this.message.title, + action: 'delete' + }, + 'tux-editor-message-tools-delete' + ); + + // Hide these links if the translation doesn't actually exist. + // They will be shown when a translation will be created. + if ( this.message.translation === null ) { + $historyItem.addClass( 'hide' ); + $deleteItem.addClass( 'hide' ); + } else if ( !this.canDelete ) { + $deleteItem.addClass( 'hide' ); + } + + // A link to Special:Translations, + // with translations of this message to other languages + $translationsItem = this.createMessageToolsItem( + 'message-tools-translations', + { + title: 'Special:Translations', + message: this.message.title + }, + 'tux-editor-message-tools-translations' + ); + + $linkToThisItem = this.createMessageToolsItem( + 'message-tools-linktothis', + { + title: 'Special:Translate', + showMessage: this.message.key, + group: this.message.primaryGroup + }, + 'tux-editor-message-tools-linktothis' + ); + + return $( '<ul>' ) + .addClass( 'tux-dropdown-menu tux-message-tools-menu hide' ) + .append( $editItem, $historyItem, $deleteItem, $translationsItem, $linkToThisItem ); + }, + + prepareEditorColumn: function () { + var translateEditor = this, + sourceString, + originalTranslation, + $editorColumn, + $messageKeyLabel, + $moreWarningsTab, + $warnings, + $warningsBlock, + $editAreaBlock, + $textarea, + $controlButtonBlock, + $editingButtonBlock, + $pasteOriginalButton, + $editSummary, + $editSummaryBlock, + $discardChangesButton = $( [] ), + $saveButton, + $requestRight, + $skipButton, + $cancelButton, + $sourceString, + $closeIcon, + $layoutActions, + $infoToggleIcon, + $messageList, + targetLangAttrib, targetLangDir, targetLangCode, prefix, + $messageTools = translateEditor.createMessageTools(), + canTranslate = mw.translate.canTranslate(); + + $editorColumn = $( '<div>' ) + .addClass( 'seven columns editcolumn' ); + + $messageKeyLabel = $( '<div>' ) + .addClass( 'ten columns messagekey' ) + .text( this.message.title ) + .append( + $( '<span>' ).addClass( 'caret' ), + $messageTools + ) + .on( 'click', function ( e ) { + $messageTools.toggleClass( 'hide' ); + e.stopPropagation(); + } ); + + $closeIcon = $( '<span>' ) + .addClass( 'one column close' ) + .attr( 'title', mw.msg( 'tux-editor-close-tooltip' ) ) + .on( 'click', function ( e ) { + translateEditor.hide(); + e.stopPropagation(); + } ); + + $infoToggleIcon = $( '<span>' ) + // Initially the editor column is contracted, + // so show the expand button first + .addClass( 'one column editor-info-toggle editor-expand' ) + .attr( 'title', mw.msg( 'tux-editor-expand-tooltip' ) ) + .on( 'click', function ( e ) { + translateEditor.infoToggle( $( this ) ); + e.stopPropagation(); + } ); + + $layoutActions = $( '<div>' ) + .addClass( 'two columns layout-actions' ) + .append( $closeIcon, $infoToggleIcon ); + + $editorColumn.append( $( '<div>' ) + .addClass( 'row tux-editor-titletools' ) + .append( $messageKeyLabel, $layoutActions ) + ); + + $messageList = $( '.tux-messagelist' ); + originalTranslation = this.message.translation; + sourceString = this.message.definition; + $sourceString = $( '<span>' ) + .addClass( 'twelve columns sourcemessage' ) + .attr( { + lang: $messageList.data( 'sourcelangcode' ), + dir: $messageList.data( 'sourcelangdir' ) + } ) + .text( sourceString ); + + // Adjust the font size for the message string based on the length + if ( sourceString.length > 100 && sourceString.length < 200 ) { + $sourceString.addClass( 'long' ); + } + + if ( sourceString.length > 200 ) { + $sourceString.addClass( 'longer' ); + } + + $editorColumn.append( $( '<div>' ) + .addClass( 'row' ) + .append( $sourceString ) + ); + + $warnings = $( '<div>' ) + .addClass( 'tux-warning hide' ); + + $moreWarningsTab = $( '<div>' ) + .addClass( 'tux-more-warnings hide' ) + .on( 'click', function () { + var $this = $( this ), + $moreWarnings = $warnings.children(), + lastWarningIndex = $moreWarnings.length - 1; + + // If the warning list is not open, only one warning is shown + if ( $this.hasClass( 'open' ) ) { + $moreWarnings.each( function ( index, element ) { + // The first element must always be shown + if ( index ) { + $( element ).addClass( 'hide' ); + } + } ); + + $this + .removeClass( 'open' ) + .text( mw.msg( 'tux-warnings-more', lastWarningIndex ) ); + } else { + $moreWarnings.each( function ( index, element ) { + // The first element must always be shown + if ( index ) { + $( element ).removeClass( 'hide' ); + } + } ); + + $this + .addClass( 'open' ) + .text( mw.msg( 'tux-warnings-hide' ) ); + } + } ); + + targetLangCode = $messageList.data( 'targetlangcode' ); + if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + targetLangAttrib = mw.config.get( 'wgContentLanguage' ); + targetLangDir = $.uls.data.getDir( targetLangAttrib ); + } else { + targetLangAttrib = targetLangCode; + targetLangDir = $messageList.data( 'targetlangdir' ); + } + + $textarea = $( '<textarea>' ) + .addClass( 'tux-textarea-translation' ) + .attr( { + lang: targetLangAttrib, + dir: targetLangDir + } ) + .val( this.message.translation || '' ); + + if ( mw.translate.isPlaceholderSupported( $textarea ) ) { + $textarea.prop( 'placeholder', mw.msg( 'tux-editor-placeholder' ) ); + } + + // Shortcuts for various insertable things + $textarea.on( 'keyup keydown', function ( e ) { + var index, info, direction; + + if ( e.type === 'keydown' && e.altKey === true ) { + // Up and down arrows + if ( e.keyCode === 38 || e.keyCode === 40 ) { + direction = e.keyCode === 40 ? 1 : -1; + info = translateEditor.$editor.find( '.infocolumn' ); + info.scrollTop( info.scrollTop() + 100 * direction ); + translateEditor.showShortcuts(); + } + } + + // Move zero to last + index = e.keyCode - 49; + if ( index === -1 ) { + index = 9; + } + + // 0..9 ~ 48..57 + if ( + e.type === 'keydown' && + e.altKey === true && + e.ctrlKey === false && + e.shiftKey === false && + index >= 0 && index < 10 + ) { + e.preventDefault(); + e.stopPropagation(); + translateEditor.$editor.find( '.shortcut-activated:visible' ).eq( index ).trigger( 'click' ); + // Update numbers and locations after trigger should be completed + window.setTimeout( function () { + translateEditor.showShortcuts(); + }, 100 ); + } + + if ( e.which === 18 && e.type === 'keyup' ) { + translateEditor.hideShortcuts(); + } else if ( e.which === 18 && e.type === 'keydown' ) { + translateEditor.showShortcuts(); + } + } ); + + $textarea.on( 'textchange', function () { + var $textarea = $( this ), + $saveButton = translateEditor.$editor.find( '.tux-editor-save-button' ), + $pasteSourceButton = translateEditor.$editor.find( '.tux-editor-paste-original-button' ), + original = translateEditor.message.translation || '', + current = $textarea.val() || ''; + + if ( original !== '' ) { + $discardChangesButton.removeClass( 'hide' ); + } + + /* Avoid Unsaved marking when translated message is not changed in content. + * - translateEditor.dirty: internal book keeping + * - mw.translate.dirty: "you have unchanged edits" warning + */ + if ( original === current ) { + translateEditor.markUnunsaved(); + } else { + translateEditor.dirty = true; + mw.translate.dirty = true; + } + + translateEditor.makeSaveButtonJustSave( $saveButton ); + + // When there is content in the editor enable the button. + // But do not enable when some saving is not finished yet. + if ( current.trim() && !translateEditor.saving ) { + $pasteSourceButton.addClass( 'hide' ); + $saveButton.prop( 'disabled', false ); + } else { + $saveButton.prop( 'disabled', true ); + $pasteSourceButton.removeClass( 'hide' ); + } + + translateEditor.resizeInsertables( $textarea ); + + translateEditor.delayValidation( function () { + translateEditor.validateTranslation(); + }, 500 ); + } ); + + $warningsBlock = $( '<div>' ) + .addClass( 'tux-warnings-block' ) + .append( $moreWarningsTab, $warnings ); + + $editAreaBlock = $( '<div>' ) + .addClass( 'row tux-editor-editarea-block' ) + .append( $( '<div>' ) + .addClass( 'editarea twelve columns' ) + .append( $warningsBlock, $textarea ) + ); + + $editorColumn.append( $editAreaBlock ); + + if ( canTranslate ) { + $pasteOriginalButton = $( '<button>' ) + .addClass( 'tux-editor-paste-original-button' ) + .text( mw.msg( 'tux-editor-paste-original-button-label' ) ) + .on( 'click', function () { + $textarea + .focus() + .val( sourceString ) + .trigger( 'input' ); + + $pasteOriginalButton.addClass( 'hide' ); + } ); + + $editSummary = $( '<input>' ) + .addClass( 'tux-input-editsummary' ) + .attr( { + maxlength: 255, + disabled: true, + placeholder: mw.msg( 'tux-editor-editsummary-placeholder' ) + } ) + .val( '' ); + + // Enable edit summary if there was a change to translation area + // or disable if there is no text in translation area + $textarea.on( 'textchange', function () { + if ( $editSummary.prop( 'disabled' ) ) { + $editSummary.prop( 'disabled', false ); + } + if ( $textarea.val().trim() === '' ) { + $editSummary.prop( 'disabled', true ); + } + } ).on( 'keydown', function ( e ) { + if ( !e.ctrlKey || e.keyCode !== 13 ) { + return; + } + + if ( !$saveButton.is( ':disabled' ) ) { + $saveButton.click(); + return; + } + $skipButton.click(); + } ); + + if ( originalTranslation !== null ) { + $discardChangesButton = $( '<button>' ) + .addClass( 'tux-editor-discard-changes-button hide' ) // Initially hidden + .text( mw.msg( 'tux-editor-discard-changes-button-label' ) ) + .on( 'click', function () { + // Restore the translation + $textarea + .focus() + .val( originalTranslation ); + + // and go back to hiding. + $discardChangesButton.addClass( 'hide' ); + + // There's nothing new to save... + $editSummary.val( '' ).prop( 'disabled', true ); + $saveButton.prop( 'disabled', true ); + // ...unless there is other action + translateEditor.makeSaveButtonContextSensitive( $saveButton ); + + translateEditor.markUnunsaved(); + translateEditor.resizeInsertables( $textarea ); + } ); + } + + if ( this.message.translation ) { + $pasteOriginalButton.addClass( 'hide' ); + } + + $editingButtonBlock = $( '<div>' ) + .addClass( 'twelve columns tux-editor-insert-buttons' ) + .append( + $pasteOriginalButton, + $discardChangesButton + ); + + $editSummaryBlock = $( '<div>' ) + .addClass( 'row tux-editor-editsummary-block' ) + .append( + $( '<div>' ) + .addClass( 'twelve columns' ) + .append( $editSummary ) + ); + + $requestRight = $( [] ); + + $saveButton = $( '<button>' ) + .prop( 'disabled', true ) + .addClass( 'tux-editor-save-button mw-ui-button mw-ui-progressive' ) + .text( mw.msg( 'tux-editor-save-button-label' ) ) + .on( 'click', function ( e ) { + translateEditor.save(); + e.stopPropagation(); + } ); + + this.makeSaveButtonContextSensitive( $saveButton, this.$messageItem ); + } else { + $editingButtonBlock = $( [] ); + + $editSummaryBlock = $( [] ); + + $requestRight = $( '<span>' ) + .addClass( 'tux-editor-request-right' ) + .text( mw.msg( 'translate-edit-nopermission' ) ); + // Make sure wgTranslatePermissionUrl setting is not 'false' + if ( mw.config.get( 'wgTranslatePermissionUrl' ) !== false ) { + $requestRight + .append( $( '<a>' ) + .text( mw.msg( 'translate-edit-askpermission' ) ) + .addClass( 'tux-editor-ask-permission' ) + .attr( { + href: mw.util.getUrl( + mw.config.get( 'wgTranslateUseSandbox' ) ? + 'Special:TranslationStash' : + mw.config.get( 'wgTranslatePermissionUrl' ) + ) + } ) + ); + } + // Disable the text area if user has no translation rights. + // Use readonly to allow copy-pasting (except for placeholders) + $textarea.prop( 'readonly', true ); + + $saveButton = $( [] ); + } + + $skipButton = $( '<button>' ) + .addClass( 'tux-editor-skip-button mw-ui-button mw-ui-quiet' ) + .text( mw.msg( 'tux-editor-skip-button-label' ) ) + .on( 'click', function ( e ) { + translateEditor.skip(); + translateEditor.next(); + + if ( translateEditor.options.onSkip ) { + translateEditor.options.onSkip.call( translateEditor ); + } + + e.stopPropagation(); + } ); + + // This appears instead of "Skip" on the last message on the page + $cancelButton = $( '<button>' ) + .addClass( 'tux-editor-cancel-button mw-ui-button mw-ui-quiet' ) + .text( mw.msg( 'tux-editor-cancel-button-label' ) ) + .on( 'click', function ( e ) { + translateEditor.skip(); + translateEditor.hide(); + + e.stopPropagation(); + } ); + + $controlButtonBlock = $( '<div>' ) + .addClass( 'twelve columns tux-editor-control-buttons' ) + .append( $requestRight, $saveButton, $skipButton, $cancelButton ); + + $editorColumn.append( $( '<div>' ) + .addClass( 'row tux-editor-actions-block' ) + .append( $editingButtonBlock ) + ); + + $editorColumn.append( $editSummaryBlock ); + + $editorColumn.append( $( '<div>' ) + .addClass( 'row tux-editor-actions-block' ) + .append( $controlButtonBlock ) + ); + + if ( canTranslate ) { + prefix = $.fn.updateTooltipAccessKeys.getAccessKeyPrefix(); + $editorColumn.append( $( '<div>' ) + .addClass( 'row shortcutinfo' ) + .text( mw.msg( + 'tux-editor-shortcut-info', + 'CTRL-ENTER', + ( prefix + 'd' ).toUpperCase(), + 'ALT', + ( prefix + 'b' ).toUpperCase() + ) ) + ); + } + + return $editorColumn; + }, + + /** + * Modifies the save button to provide suitable default action for *unchanged* + * message. It will revert back to normal save button if the text is changed. + * + * @param {jQuery} $button The save button. + */ + makeSaveButtonContextSensitive: function ( $button ) { + var self = this; + + if ( this.message.properties.status === 'fuzzy' ) { + $button.prop( 'disabled', false ); + $button.text( mw.msg( 'tux-editor-confirm-button-label' ) ); + $button.off( 'click' ); + $button.on( 'click', function ( e ) { + self.save(); + e.stopPropagation(); + } ); + } else if ( this.message.proofreadable ) { + $button.prop( 'disabled', false ); + $button.text( mw.msg( 'tux-editor-proofread-button-label' ) ); + $button.off( 'click' ); + $button.on( 'click', function ( e ) { + $button.prop( 'disabled', true ); + self.message.proofreadAction(); + self.next(); + e.stopPropagation(); + } ); + } + }, + + /** + * Modifies the save button to just save the translation as usual. Whether the + * button is enabled or not is controlled elsewhere. + * + * @param {jQuery} $button The save button. + */ + makeSaveButtonJustSave: function ( $button ) { + var self = this; + + $button.text( mw.msg( 'tux-editor-save-button-label' ) ); + $button.off( 'click' ); + $button.on( 'click', function ( e ) { + self.save(); + e.stopPropagation(); + } ); + }, + + /** + * Validate the current translation using the API + * and show the warnings if necessary. + */ + validateTranslation: function () { + var translateEditor = this, + api, + $textarea = translateEditor.$editor.find( '.tux-textarea-translation' ); + + api = new mw.Api(); + + api.post( { + action: 'translationcheck', + title: this.message.title, + translation: $textarea.val() + } ).done( function ( data ) { + var warningIndex, + warnings = data.warnings; + + translateEditor.removeWarning( 'validation' ); + if ( !warnings || !warnings.length ) { + return; + } + + // Remove useless fuzzy warning if we have more details + translateEditor.removeWarning( 'fuzzy' ); + + // Disable confirm translation button, since fuzzy translations + // cannot be confirmed. The check for dirty state can be removed + // to prevent translations with warnings. + if ( !translateEditor.dirty ) { + translateEditor.$editor.find( '.tux-editor-save-button' ) + .prop( 'disabled', true ); + } + + for ( warningIndex = 0; warningIndex < warnings.length; warningIndex++ ) { + translateEditor.addWarning( warnings[ warningIndex ], 'validation' ); + } + } ); + }, + + /** + * Remove all warning of given type + * + * @param {string} type + */ + removeWarning: function ( type ) { + var $tuxWarning = this.$editor.find( '.tux-warning' ); + + $tuxWarning.find( '.' + type ).remove(); + if ( !$tuxWarning.children().length ) { + this.$editor.find( '.tux-more-warnings' ).addClass( 'hide' ); + } + }, + + /** + * Displays the supplied warning above the translation edit area. + * Newer warnings are added to the top while older warnings are + * added to the bottom. This also means that older warnings will + * not be shown by default unless the user clicks "more warnings" tab. + * + * @param {string} warning used as html for the warning display + * @param {string} type used to group the warnings.eg: validation, diff, error + * @return {jQuery} the new warning element + */ + addWarning: function ( warning, type ) { + var warningCount, + $warnings = this.$editor.find( '.tux-warning' ), + $moreWarningsTab = this.$editor.find( '.tux-more-warnings' ), + $newWarning = $( '<div>' ) + .addClass( 'tux-warning-message ' + type ) + .html( warning ); + + this.$editor.find( '.tux-warning-message' ).addClass( 'hide' ); + + $warnings + .removeClass( 'hide' ) + .prepend( $newWarning ); + + warningCount = $warnings.find( '.tux-warning-message' ).length; + + if ( warningCount > 1 ) { + $moreWarningsTab + .text( mw.msg( 'tux-warnings-more', warningCount - 1 ) ) + .removeClass( 'hide open' ); + } else { + $moreWarningsTab.addClass( 'hide' ); + } + + return $newWarning; + }, + + prepareInfoColumn: function () { + var $messageDescEditor, $messageDescTextarea, + $messageDescSaveButton, $messageDescCancelButton, + $messageDescViewer, + $infoColumn = $( '<div>' ).addClass( 'infocolumn' ), + translateEditor = this; + + $infoColumn.append( $( '<div>' ) + .addClass( 'row loading' ) + .text( mw.msg( 'tux-editor-loading' ) ) + ); + + if ( mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + $messageDescSaveButton = $( '<button>' ) + .addClass( 'tux-editor-savedoc-button mw-ui-button mw-ui-progressive' ) + .prop( 'disabled', true ) + .text( mw.msg( 'tux-editor-doc-editor-save' ) ) + .on( 'click', function () { + translateEditor.saveDocumentation() + .done( function () { + var $descEditLink = $messageDescViewer.find( '.message-desc-edit' ); + $descEditLink.text( mw.msg( 'tux-editor-edit-desc' ) ); + } ); + } ); + + $messageDescCancelButton = $( '<button>' ) + .addClass( 'tux-editor-skipdoc-button mw-ui-button mw-ui-quiet' ) + .text( mw.msg( 'tux-editor-doc-editor-cancel' ) ) + .on( 'click', function () { + translateEditor.hideDocumentationEditor(); + } ); + + $messageDescTextarea = $( '<textarea>' ) + .addClass( 'tux-textarea-documentation' ) + .on( 'textchange', function () { + $messageDescSaveButton.prop( 'disabled', false ); + } ); + + if ( mw.translate.isPlaceholderSupported( $messageDescTextarea ) ) { + $messageDescTextarea.prop( 'placeholder', mw.msg( 'tux-editor-doc-editor-placeholder' ) ); + } + + $messageDescEditor = $( '<div>' ) + .addClass( 'row message-desc-editor hide' ) + .append( + $messageDescTextarea, + $( '<div>' ) + .addClass( 'row' ) + .append( + $messageDescSaveButton, + $messageDescCancelButton + ) + ); + + $messageDescViewer = $( '<div>' ) + .addClass( 'message-desc-viewer hide' ) + .append( + $( '<div>' ) + .addClass( 'row message-desc' ), + $( '<div>' ) + .addClass( 'row message-desc-control' ) + .append( $( '<a>' ) + .attr( { + href: mw.translate.getDocumentationEditURL( + this.message.title.replace( /\/[a-z-]+$/, '' ) + ), + target: '_blank' + } ) + .addClass( 'message-desc-edit' ) + .on( 'click', this.showDocumentationEditor.bind( this ) ) + ) + ); + + if ( !mw.translate.canTranslate() ) { + $messageDescViewer.find( '.message-desc-control' ).addClass( 'hide' ); + } + + $infoColumn.append( + $messageDescEditor, + $messageDescViewer + ); + } + + $infoColumn.append( $( '<div>' ) + .addClass( 'row uneditable-documentation hide' ) + ); + + $infoColumn.append( $( '<div>' ) + .addClass( 'row tm-suggestions-title hide' ) + .text( mw.msg( 'tux-editor-suggestions-title' ) ) + ); + + $infoColumn.append( $( '<div>' ) + .addClass( 'row in-other-languages-title hide' ) + .text( mw.msg( 'tux-editor-in-other-languages' ) ) + ); + + // The actual href is set when translationhelpers are loaded + $infoColumn.append( $( '<div>' ) + .addClass( 'row help hide' ) + .append( + $( '<span>' ) + .text( mw.msg( 'tux-editor-need-more-help' ) ), + $( '<a>' ) + .attr( { + href: '#', + target: '_blank' + } ) + .text( mw.msg( 'tux-editor-ask-help' ) ) + ) + ); + + return $( '<div>' ) + .addClass( 'five columns infocolumn-block' ) + .append( + $( '<span>' ).addClass( 'tux-message-editor__caret' ), + $infoColumn + ); + }, + + show: function () { + var $next, $textarea; + + if ( !this.$editor ) { + this.init(); + } + + $textarea = this.$editor.find( '.editcolumn textarea' ); + // Hide all other open editors in the page + $( '.tux-message.open' ).each( function () { + $( this ).data( 'translateeditor' ).hide(); + } ); + + // The access keys need to be shifted to the editor currently active + $( '.tux-editor-save-button, .tux-editor-save-button' ).removeAttr( 'accesskey' ); + this.$editor.find( '.tux-editor-save-button' ).attr( 'accesskey', 's' ); + this.$editor.find( '.tux-editor-skip-button' ).attr( 'accesskey', 'd' ); + this.$editor.find( '.tux-input-editsummary' ).attr( 'accesskey', 'b' ); + // @todo access key for the cancel button + + this.$messageItem.addClass( 'hide' ); + this.$editor.removeClass( 'hide' ); + $textarea.focus(); + + autosize( $textarea ); + this.resizeInsertables( $textarea ); + + this.shown = true; + this.$editTrigger.addClass( 'open' ); + + // don't waste time, get ready with next message + $next = this.$editTrigger.next( '.tux-message' ); + + if ( $next.length ) { + $next.data( 'translateeditor' ).init(); + } + + mw.hook( 'mw.translate.editor.afterEditorShown' ).fire( this.$editor ); + + return false; + }, + + hide: function () { + // If the user has made changes, make sure they are either + // in process of being saved or highlighted as unsaved. + if ( this.dirty ) { + if ( this.saving ) { + this.markSaving(); + } else { + this.markUnsaved(); + } + } + + if ( this.$editor ) { + this.$editor.addClass( 'hide' ); + } + + this.hideShortcuts(); + this.$editTrigger.removeClass( 'open' ); + this.$messageItem.removeClass( 'hide' ); + this.shown = false; + + return false; + }, + + infoToggle: function ( toggleIcon ) { + if ( this.expanded ) { + this.contract( toggleIcon ); + } else { + this.expand( toggleIcon ); + } + }, + + contract: function ( toggleIcon ) { + // Change the icon image + toggleIcon + .removeClass( 'editor-contract' ) + .addClass( 'editor-expand' ) + .attr( 'title', mw.msg( 'tux-editor-expand-tooltip' ) ); + + this.$editor.removeClass( 'tux-message-editor--expanded' ); + this.expanded = false; + }, + + expand: function ( toggleIcon ) { + // Change the icon image + toggleIcon + .removeClass( 'editor-expand' ) + .addClass( 'editor-contract' ) + .attr( 'title', mw.msg( 'tux-editor-collapse-tooltip' ) ); + + this.$editor.addClass( 'tux-message-editor--expanded' ); + this.expanded = true; + }, + + /** + * Adds the diff between old and current definitions to the view. + * + * @param {Object} definitiondiff A definitiondiff object as returned by API. + */ + addDefinitionDiff: function ( definitiondiff ) { + var $trigger; + + if ( !definitiondiff || definitiondiff.error ) { + mw.log( 'Error loading translation diff ' + definitiondiff && definitiondiff.error ); + return; + } + + // Load the diff styles + mw.loader.load( 'mediawiki.diff.styles' ); + + $trigger = $( '<span>' ) + .addClass( 'show-diff-link' ) + .text( mw.msg( 'tux-editor-outdated-warning-diff-link' ) ) + .on( 'click', function () { + $( this ).parent().html( definitiondiff.html ); + } ); + + this.removeWarning( 'fuzzy' ); + this.addWarning( + mw.message( 'tux-editor-outdated-warning' ).escaped(), + 'diff' + ).append( $trigger ); + }, + + /** + * Attach event listeners + */ + listen: function () { + var translateEditor = this; + + this.$editTrigger.find( '.tux-message-item' ).click( function () { + translateEditor.show(); + + return false; + } ); + }, + + /** + * Makes the textare large enough for insertables and positions the insertables. + * + * @param {jQuery} $textarea Text area. + */ + resizeInsertables: function ( $textarea ) { + var $buttonArea, buttonAreaHeight; + + $buttonArea = this.$editor.find( '.tux-editor-insert-buttons' ); + buttonAreaHeight = $buttonArea.height(); + $textarea.css( 'padding-bottom', buttonAreaHeight + 5 ); + $buttonArea.css( 'top', -buttonAreaHeight ); + autosize.update( $textarea ); + } + }; + + /* + * translateeditor PLUGIN DEFINITION + */ + + $.fn.translateeditor = function ( options ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'translateeditor' ); + + if ( !data ) { + $this.data( 'translateeditor', + ( data = new TranslateEditor( this, options ) ) + ); + } + + if ( typeof options === 'string' ) { + data[ options ].call( $this ); + } + } ); + }; + + mw.translate.editor = mw.translate.editor || {}; + mw.translate.editor = $.extend( TranslateEditor.prototype, mw.translate.editor ); + + function delayer() { + return ( function () { + var timer = 0; + + return function ( callback, milliseconds ) { + clearTimeout( timer ); + timer = setTimeout( callback, milliseconds ); + }; + }() ); + } +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.editor.shortcuts.js b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.shortcuts.js new file mode 100644 index 00000000..95dc9a47 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.shortcuts.js @@ -0,0 +1,71 @@ +/*! + * Translate editor shortcuts + */ +( function () { + 'use strict'; + var translateEditorShortcuts = { + showShortcuts: function () { + var editorOffset, minTop, maxTop, maxLeft, middle, rtl; + + // Any better way? + rtl = $( 'body' ).is( '.rtl' ); + + editorOffset = this.$editor.offset(); + minTop = editorOffset.top; + maxTop = minTop + this.$editor.outerHeight(); + middle = minTop + ( maxTop - minTop ) / 2; + maxLeft = rtl ? editorOffset.left : editorOffset.left + this.$editor.outerWidth(); + + this.hideShortcuts(); + + // For scrolling up and down + $( '<div>' ) + .text( '↑' ) + .addClass( 'shortcut-popup' ) + .appendTo( 'body' ) + .offset( { top: middle - 15, left: maxLeft } ) + .css( 'transform', 'translate( -50%, 0 )' ); + + $( '<div>' ) + .text( '↓' ) + .addClass( 'shortcut-popup' ) + .appendTo( 'body' ) + .offset( { top: middle + 15, left: maxLeft } ) + .css( 'transform', 'translate( -50%, 0 )' ); + + this.$editor.find( '.shortcut-activated:visible' ).each( function ( index ) { + var offset = getStartCornerOffsetOf( $( this ), rtl ); + + // Let's not have numbers appear outside the editor over other content + if ( offset.top > maxTop || offset.top < minTop ) { + return; + } + + $( '<div>' ) + .text( index + 1 ) + .addClass( 'shortcut-popup' ) + .appendTo( 'body' ) + .offset( offset ) + .css( 'transform', 'translate( -50%, -50% )' ); + } ); + }, + + hideShortcuts: function () { + $( '.shortcut-popup' ).remove(); + } + }; + + function getStartCornerOffsetOf( $element, rtl ) { + var offset = $element.offset(); + + if ( rtl ) { + offset.left += $element.outerWidth(); + } + + return offset; + } + + mw.translate.editor = mw.translate.editor || {}; + $.extend( mw.translate.editor, translateEditorShortcuts ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js b/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js new file mode 100644 index 00000000..eda0a43b --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js @@ -0,0 +1,633 @@ +( function () { + 'use strict'; + + var groupsLoader, delay; + + /** + * options + * - position: accepts same values as jquery.ui.position + * - onSelect: + * - language: + * - preventSelector: boolean to load but not show the group selector. + * - recent: list of recent group ids + * groups: list of message group ids + * + * @param {Element} element + * @param {Object} options + * @param {Object} [options.position] Accepts same values as jquery.ui.position. + * @param {Function} [options.onSelect] Callback with message group id when selected. + * @param {string} options.language Language code for statistics. + * @param {boolean} [options.preventSelector] Whether not to show the group selector. + * @param {string[]} [options.recent] List of recent message group ids. + * @param {string[]} [groups] List of message group ids to show. + */ + function TranslateMessageGroupSelector( element, options, groups ) { + this.$trigger = $( element ); + this.$menu = null; + this.$search = null; + this.$list = null; + this.$loader = null; + + this.parentGroupId = null; + this.options = $.extend( true, {}, $.fn.msggroupselector.defaults, options ); + // Store the explicitly given options, which can be passed to subgroup + // selectors. + this.customOptions = options; + this.flatGroupList = null; + this.groups = groups; + this.firstShow = true; + + this.init(); + } + + TranslateMessageGroupSelector.prototype = { + constructor: TranslateMessageGroupSelector, + + /** + * Initialize the plugin + */ + init: function () { + this.parentGroupId = this.$trigger.data( 'msggroupid' ); + this.prepareSelectorMenu(); + this.listen(); + }, + + /** + * Prepare the selector menu rendering + */ + prepareSelectorMenu: function () { + var $listFilters, + $listFiltersGroup, + $search, + $searchIcon, + $searchGroup; + + this.$menu = $( '<div>' ) + .addClass( 'tux-groupselector' ) + .addClass( 'grid' ); + + $searchIcon = $( '<div>' ) + .addClass( 'two columns tux-groupselector__filter__search__icon' ); + + this.$search = $( '<input>' ) + .prop( 'type', 'text' ) + .addClass( 'tux-groupselector__filter__search__input' ); + + if ( mw.translate.isPlaceholderSupported( this.$search ) ) { + this.$search.prop( 'placeholder', mw.msg( 'translate-msggroupselector-search-placeholder' ) ); + } + + $search = $( '<div>' ) + .addClass( 'ten columns' ) + .append( this.$search ); + + $listFilters = $( '<div>' ) + .addClass( 'tux-groupselector__filter__tabs' ) + .addClass( 'six columns' ) + .append( + $( '<div>' ) + .addClass( 'tux-grouptab tux-grouptab--all tux-grouptab--selected' ) + .text( mw.msg( 'translate-msggroupselector-search-all' ) ) + ); + + if ( this.options.recent && this.options.recent.length ) { + $listFilters.append( + $( '<div>' ) + .addClass( 'tux-grouptab tux-grouptab--recent' ) + .text( mw.msg( 'translate-msggroupselector-search-recent' ) ) + ); + } + + $searchGroup = $( '<div>' ) + .addClass( 'tux-groupselector__filter__search' ) + .addClass( 'six columns' ) + .append( $searchIcon, $search ); + + $listFiltersGroup = $( '<div>' ) + .addClass( 'tux-groupselector__filter' ) + .addClass( 'row' ) + .append( $listFilters, $searchGroup ); + + this.$list = $( '<div>' ) + .addClass( 'tux-grouplist' ) + .addClass( 'row' ); + + this.$loader = $( '<div>' ) + .addClass( 'tux-loading-indicator tux-loading-indicator--centered' ); + + this.$menu.append( $listFiltersGroup, this.$loader, this.$list ); + + $( 'body' ).append( this.$menu ); + }, + + /** + * Show the selector + */ + show: function () { + this.$menu.addClass( 'open' ).show(); + this.position(); + // Place the focus in the message group search box. + this.$search.focus(); + // Start loading the groups, but assess the situation again after + // they are loaded, in case user has made further interactions. + if ( this.firstShow ) { + this.loadGroups().done( this.showList.bind( this ) ); + this.firstShow = false; + } + }, + + /** + * Hide the selector + * + * @param {jQuery.Event} e + */ + hide: function ( e ) { + // Do not hide if the trigger is clicked + if ( e && ( this.$trigger.is( e.target ) || this.$trigger.has( e.target ).length ) ) { + return; + } + + this.$menu.hide().removeClass( 'open' ); + }, + + /** + * Toggle the menu open/close state + */ + toggle: function () { + if ( this.$menu.hasClass( 'open' ) ) { + this.hide(); + } else { + this.show(); + } + }, + + /** + * Attach event listeners + */ + listen: function () { + var $tabs, + groupSelector = this; + + // Hide the selector panel when clicking outside of it + $( 'html' ).on( 'click', this.hide.bind( this ) ); + + groupSelector.$trigger.on( 'click', function () { + groupSelector.toggle(); + } ); + + groupSelector.$menu.on( 'click', function ( e ) { + e.preventDefault(); + e.stopPropagation(); + } ); + + // Handle click on row item. This selects the group, and in case it has + // subgroups, also opens a new menu to show them. + groupSelector.$menu.on( 'click', '.tux-grouplist__item', function () { + var $newLink, + messageGroup = $( this ).data( 'msggroup' ); + + groupSelector.hide(); + + groupSelector.$trigger.nextAll().remove(); + + if ( !groupSelector.options.preventSelector ) { + $newLink = $( '<span>' ) + .addClass( 'grouptitle grouplink' ) + .text( messageGroup.label ) + .data( 'msggroupid', messageGroup.id ); + + groupSelector.$trigger.after( $newLink ); + + if ( messageGroup.groups && messageGroup.groups.length > 0 ) { + // Show the new menu immediately. + // Pass options for callbacks, language etc. but ignore the position + // option unless explicitly given to allow automatic recalculation + // of the position compared to the new trigger. + $newLink + .addClass( 'tux-breadcrumb__item--aggregate' ) + .msggroupselector( groupSelector.customOptions ) + .data( 'msggroupselector' ).show(); + } + } + + if ( groupSelector.options.onSelect ) { + groupSelector.options.onSelect( messageGroup ); + } + } ); + + // Handle the tabs All | Recent + $tabs = groupSelector.$menu.find( '.tux-grouptab' ); + $tabs.on( 'click', function () { + var $this = $( this ); + + /* Do nothing if user clicks the active tab. + * Fixes two things: + * - The blue bottom border highlight doesn't jump around + * - No flash when clicking recent tab again + */ + if ( $this.hasClass( 'tux-grouptab--selected' ) ) { + return; + } + + // This is okay as long as we only have two classes + $tabs.toggleClass( 'tux-grouptab--selected' ); + groupSelector.$search.val( '' ); + groupSelector.showList(); + } ); + + this.$search.on( 'click', this.show.bind( this ) ) + .on( 'keypress', this.keyup.bind( this ) ) + .on( 'keyup', this.keyup.bind( this ) ); + + if ( this.eventSupported( 'keydown' ) ) { + this.$search.on( 'keydown', this.keyup.bind( this ) ); + } + }, + + /** + * Handle the keypress/keyup events in the message group search box. + */ + keyup: function () { + delay( this.showList.bind( this ), 300 ); + }, + + /** + * Position the menu + */ + position: function () { + if ( this.options.position.of === undefined ) { + this.options.position.of = this.$trigger; + } + this.$menu.position( this.options.position ); + }, + + /** + * Shows suitable list for current view, taking possible filter into account + */ + showList: function () { + var query = this.$search.val().trim().toLowerCase(); + + if ( query ) { + this.filter( query ); + } else { + this.showUnfilteredList(); + } + }, + + /** + * Shows an unfiltered list of groups depending on the selected tab. + */ + showUnfilteredList: function () { + var $selected = this.$menu.find( '.tux-grouptab--selected' ); + + if ( $selected.hasClass( 'tux-grouptab--all' ) ) { + if ( this.groups ) { + this.showSelectedGroups( this.groups ); + } else { + this.showDefaultGroups(); + } + } else if ( $selected.hasClass( 'tux-grouptab--recent' ) ) { + this.showRecentGroups(); + } + }, + + /** + * Shows the list of message groups excluding subgroups. + * + * In case a parent message group has been given, only subgroups of that + * message group are shown, otherwise all top-level message groups are shown. + */ + showDefaultGroups: function () { + var groupSelector = this; + + this.$loader.show(); + + this.loadGroups().done( function ( groups ) { + var groupsToShow = mw.translate.findGroup( groupSelector.parentGroupId, groups ); + + // We do not want to display the group itself, only its subgroups + if ( groupSelector.parentGroupId ) { + groupsToShow = groupsToShow.groups; + } + + groupSelector.$loader.hide(); + groupSelector.$list.empty(); + groupSelector.addGroupRows( groupsToShow ); + } ); + }, + + /** + * Show recent message groups. + */ + showRecentGroups: function () { + var recent = this.options.recent || []; + + this.showSelectedGroups( recent ); + }, + + /** + * Load message groups. + * + * @param {Array} groups List of the message group ids to show. + */ + showSelectedGroups: function ( groups ) { + var groupSelector = this; + this.$loader.show(); + this.loadGroups() + .then( function ( allGroups ) { + var rows = []; + $.each( groups, function ( index, id ) { + var group = mw.translate.findGroup( id, allGroups ); + if ( group ) { + rows.push( groupSelector.prepareMessageGroupRow( group ) ); + } + } ); + return rows; + } ) + .always( function () { + groupSelector.$loader.hide(); + groupSelector.$list.empty(); + } ) + .done( function ( rows ) { + groupSelector.$list.append( rows ); + } ); + }, + + /** + * Flattens a message group tree. + * + * @param {Array} messageGroups An array or data object. + * @param {Object} foundIDs The array in which the keys are IDs of message groups that were found already. + */ + flattenGroupList: function ( messageGroups, foundIDs ) { + var i; + + if ( messageGroups.groups ) { + messageGroups = messageGroups.groups; + } + + for ( i = 0; i < messageGroups.length; i++ ) { + // Avoid duplicate groups, and add the parent before subgroups + if ( !foundIDs[ messageGroups[ i ].id ] ) { + this.flatGroupList.push( messageGroups[ i ] ); + foundIDs[ messageGroups[ i ].id ] = true; + } + + // In case there are subgroups, add them recursively + if ( messageGroups[ i ].groups ) { + this.flattenGroupList( messageGroups[ i ].groups, foundIDs ); + } + } + }, + + /** + * Search the message groups based on label or id. + * Label match is prefix match, while id match is exact match. + * + * @param {string} query + */ + filter: function ( query ) { + var self = this; + + this.loadGroups().done( function ( groups ) { + var currentGroup, index, matcher, foundGroups = []; + + if ( !self.flatGroupList ) { + self.flatGroupList = []; + currentGroup = mw.translate.findGroup( self.parentGroupId, groups ); + if ( self.parentGroupId ) { + currentGroup = currentGroup.groups; + } + self.flattenGroupList( currentGroup, {} ); + } + + // Optimization, assuming that people search the beginning + // of the group name. + matcher = new RegExp( '\\b' + escapeRegex( query ), 'i' ); + + for ( index = 0; index < self.flatGroupList.length; index++ ) { + if ( matcher.test( self.flatGroupList[ index ].label ) || + query === self.flatGroupList[ index ].id ) { + foundGroups.push( self.flatGroupList[ index ] ); + } + } + + self.$loader.hide(); + self.$list.empty(); + self.addGroupRows( foundGroups ); + } ); + }, + + /** + * Load message groups and relevant properties using the API. + * + * @return {jQuery.Promise} + */ + loadGroups: function () { + var params; + + if ( groupsLoader !== undefined ) { + return groupsLoader; + } + + params = { + action: 'query', + meta: 'messagegroups', + mgformat: 'tree', + mgprop: 'id|label|icon|priority|prioritylangs|priorityforce', + mgiconsize: '32' + }; + + groupsLoader = new mw.Api() + .get( params ) + .then( function ( result ) { + return result.query.messagegroups; + } ) + .promise(); + + return groupsLoader; + }, + + /** + * Add rows with message groups to the selector. + * + * @param {Array} groups Array of message group objects to add. + */ + addGroupRows: function ( groups ) { + var groupSelector = this, + $msgGroupRows = [], + $parent, + targetLanguage = this.options.language; + + if ( !groups ) { + return; + } + + $.each( groups, function ( index, group ) { + /* Hide from the selector: + * - discouraged groups (the only priority value currently supported). + * - groups that are recommended for other languages. + */ + if ( group.priority === 'discouraged' || + ( group.priorityforce && + group.prioritylangs && + group.prioritylangs.indexOf( targetLanguage ) === -1 ) + ) { + return; + } + + $msgGroupRows.push( groupSelector.prepareMessageGroupRow( group ) ); + } ); + + if ( this.parentGroupId ) { + $parent = this.$list.find( '.tux-grouplist__item[data-msggroupid="' + + this.parentGroupId + '"]' ); + + if ( $parent.length ) { + $parent.after( $msgGroupRows ); + return; + } + } + + this.$list.append( $msgGroupRows ); + }, + + /** + * Prepare a message group row in the selector. + * + * @param {Object} messagegroup object. + * @return {Object} a jQuery object with the groups selector row (<div>). + */ + prepareMessageGroupRow: function ( messagegroup ) { + var $row, + $icon, + $label, + $statsbar, + $subGroupsLabel, + style = ''; + + $row = $( '<div>' ) + .addClass( 'row tux-grouplist__item' ) + .attr( 'data-msggroupid', messagegroup.id ) + .data( 'msggroup', messagegroup ); + + $icon = $( '<div>' ) + .addClass( 'tux-grouplist__item__icon' ) + .addClass( 'one column' ); + + $statsbar = $( '<div>' ).languagestatsbar( { + language: this.options.language, + group: messagegroup.id + } ); + + $label = $( '<div>' ) + .addClass( 'tux-grouplist__item__label' ) + .addClass( 'seven columns' ) + .append( + $( '<span>' ) + // T130390: must be attr for IE/Edge. + .attr( { dir: 'auto' } ) + .text( messagegroup.label ), + $statsbar + ); + + if ( messagegroup.icon && messagegroup.icon.raster ) { + style += 'background-image: url(--);'; + style = style.replace( /--/g, messagegroup.icon.raster ); + } + + if ( messagegroup.icon && messagegroup.icon.vector ) { + style += 'background-image: linear-gradient(transparent, transparent), url(--);'; + style = style.replace( /--/g, messagegroup.icon.vector ); + } + + if ( style !== '' ) { + $icon.attr( 'style', style ); + } + + $subGroupsLabel = $( [] ); + + if ( messagegroup.groups && messagegroup.groups.length > 0 ) { + $subGroupsLabel = $( '<div>' ) + .addClass( 'tux-grouplist__item__subgroups' ) + .addClass( 'four columns' ) + .text( mw.msg( 'translate-msggroupselector-view-subprojects', + messagegroup.groups.length ) ); + } + + return $row.append( $icon, $label, $subGroupsLabel ); + }, + + /** + * Check that a DOM event is supported by the $menu jQuery object. + * + * @param {string} eventName + * @return {boolean} + */ + eventSupported: function ( eventName ) { + var $search = this.$menu.find( '.tux-groupselector__filter__search__input' ), + isSupported = eventName in $search; + + if ( !isSupported ) { + this.$element.setAttribute( eventName, 'return;' ); + isSupported = typeof this.$element[ eventName ] === 'function'; + } + + return isSupported; + } + }; + + /* + * msggroupselector PLUGIN DEFINITION + */ + + $.fn.msggroupselector = function ( options, groups ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'msggroupselector' ); + + if ( !data ) { + $this.data( 'msggroupselector', + ( data = new TranslateMessageGroupSelector( this, options, groups ) ) + ); + } + + if ( typeof options === 'string' ) { + data[ options ].call( $this ); + } + } ); + }; + + $.fn.msggroupselector.Constructor = TranslateMessageGroupSelector; + + $.fn.msggroupselector.defaults = { + language: 'en', + position: { + my: 'left top', + at: 'left-90 bottom+5' + } + }; + + /* + * Private functions + */ + + /** + * Escape the search query for regex match + * + * @param {string} value A search string to be escaped. + * @return {string} Escaped string that is safe to use for a search. + */ + function escapeRegex( value ) { + return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' ); + } + + delay = ( function () { + var timer = 0; + + return function ( callback, milliseconds ) { + clearTimeout( timer ); + timer = setTimeout( callback, milliseconds ); + }; + }() ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.hooks.js b/www/wiki/extensions/Translate/resources/js/ext.translate.hooks.js new file mode 100644 index 00000000..a227b304 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.hooks.js @@ -0,0 +1,37 @@ +/*! + * JavaScript hook framework for Translate (since MediaWiki code doesn't + * yet have one. See hooks.txt in Translate directory for how to use hooks. + * + * @author Harry Burt + * @license GPL-2.0-or-later + * @since 2012-08-22 + */ + +( function () { + 'use strict'; + + mw.translateHooks = { + add: function ( name, func ) { + showDeprecationWarning(); + + mw.hook( name ).add( func ); + }, + + run: function ( /* infinite list of parameters */ ) { + var args, name; + + showDeprecationWarning(); + + args = Array.prototype.slice.call( arguments ); + name = args.shift(); + + mw.hook( name ).fire( args ); + } + }; + + function showDeprecationWarning() { + mw.log.warn( '`mw.translateHooks` has been deprecated and will be removed in the ' + + 'future. Use `mw.hook` instead. See - ' + + 'https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.hook' ); + } +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.messagetable.js b/www/wiki/extensions/Translate/resources/js/ext.translate.messagetable.js new file mode 100644 index 00000000..adf3d33c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.messagetable.js @@ -0,0 +1,905 @@ +( function () { + 'use strict'; + + var itemsClass = { + proofread: '.tux-message-proofread', + page: '.tux-message-pagemode', + translate: '.tux-message' + }; + + mw.translate = mw.translate || {}; + mw.translate = $.extend( mw.translate, { + getMessages: function ( messageGroup, language, offset, limit, filter ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + list: 'messagecollection', + mcgroup: messageGroup, + mclanguage: language, + mcoffset: offset, + mclimit: limit, + mcfilter: filter, + mcprop: 'definition|translation|tags|properties', + rawcontinue: 1, + errorformat: 'html' + } ); + } + } ); + + function MessageTable( container, options, settings ) { + this.$container = $( container ); + this.options = options; + this.options = $.extend( {}, $.fn.messagetable.defaults, options ); + this.settings = settings; + // mode can be proofread, page or translate + this.mode = this.options.mode; + this.firstProofreadTipShown = false; + this.initialized = false; + this.$header = this.$container.siblings( '.tux-messagetable-header' ); + // Container is between these in the dom. + this.$loader = this.$container.siblings( '.tux-messagetable-loader' ); + this.$loaderIcon = this.$loader.find( '.tux-loading-indicator' ); + this.$loaderInfo = this.$loader.find( '.tux-messagetable-loader-info' ); + this.$actionBar = this.$container.siblings( '.tux-action-bar' ); + this.$statsBar = this.$actionBar.find( '.tux-message-list-statsbar' ); + this.$proofreadOwnTranslations = this.$actionBar.find( '.tux-proofread-own-translations-button' ); + this.messages = []; + this.loading = false; + this.init(); + this.listen(); + } + + MessageTable.prototype = { + init: function () { + this.$actionBar.removeClass( 'hide' ); + }, + + listen: function () { + var messageTable = this, + $filterInput = this.$container.parent().find( '.tux-message-filter-box' ); + + // Vector has transitions of 250ms which affect layout. Let those finish. + $( window ).on( 'scroll', $.debounce( 250, function () { + messageTable.scroll(); + + if ( isLoaderVisible( messageTable.$loader ) ) { + messageTable.load(); + } + } ) ).on( 'resize', $.throttle( 250, function () { + messageTable.resize(); + messageTable.scroll(); + } ) ); + + if ( mw.translate.isPlaceholderSupported( $filterInput ) ) { + $filterInput.prop( 'placeholder', mw.msg( 'tux-message-filter-placeholder' ) ); + } + + $filterInput.on( 'textchange', $.debounce( 250, function () { + messageTable.search( $filterInput.val() ); + } ) ); + + this.$actionBar.find( 'button.proofread-mode-button' ).on( 'click', function () { + messageTable.switchMode( 'proofread' ); + } ); + + this.$actionBar.find( 'button.translate-mode-button' ).on( 'click', function () { + messageTable.switchMode( 'translate' ); + } ); + + this.$actionBar.find( 'button.page-mode-button' ).on( 'click', function () { + messageTable.switchMode( 'page' ); + } ); + + this.$proofreadOwnTranslations.click( function () { + var $this = $( this ), + hideMessage = mw.msg( 'tux-editor-proofreading-hide-own-translations' ), + showMessage = mw.msg( 'tux-editor-proofreading-show-own-translations' ); + + if ( $this.hasClass( 'down' ) ) { + messageTable.setHideOwnInProofreading( false ); + $this.removeClass( 'down' ).text( hideMessage ); + } else { + messageTable.setHideOwnInProofreading( true ); + $this.addClass( 'down' ).text( showMessage ); + } + } ); + }, + + /** + * Clear the message table + */ + clear: function () { + this.$container.empty(); + $( '.translate-tooltip' ).remove(); + this.messages = []; + // Any ongoing loading process will notice this and will reject results. + this.loading = false; + }, + + /** + * Adds a new message using current mode. + * + * @param {Object} message + */ + add: function ( message ) { + // Prepare the message for display + mw.hook( 'mw.translate.messagetable.formatMessageBeforeTable' ).fire( message ); + + if ( this.mode === 'translate' ) { + this.addTranslate( message ); + } else if ( this.mode === 'proofread' ) { + this.addProofread( message ); + } else if ( this.mode === 'page' ) { + this.addPageModeMessage( message ); + } + }, + + /** + * Add a message to the message table for translation. + * + * @param {Object} message + */ + addTranslate: function ( message ) { + var $message, + targetLangDir, targetLangAttrib, + targetLangCode = this.$container.data( 'targetlangcode' ), + sourceLangCode = this.$container.data( 'sourcelangcode' ), + sourceLangDir = $.uls.data.getDir( sourceLangCode ), + status = message.properties.status, + statusClass = 'tux-status-' + status, + $messageWrapper = $( '<div>' ).addClass( 'row tux-message' ), + statusMsg = ''; + + message.proofreadable = false; + + if ( message.tags.length && + message.tags.indexOf( 'optional' ) >= 0 && + status === 'untranslated' + ) { + status = 'optional'; + statusClass = 'tux-status-optional'; + } + + // Fuzzy translations need warning class + if ( status === 'fuzzy' ) { + statusClass = statusClass + ' tux-warning'; + } + + // Label the status if it is not untranslated + if ( status !== 'untranslated' ) { + // Give grep a chance to find the usages: + // tux-status-optional, tux-status-fuzzy, tux-status-proofread, + // tux-status-translated, tux-status-saving, tux-status-unsaved + statusMsg = 'tux-status-' + status; + } + + if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + targetLangAttrib = mw.config.get( 'wgContentLanguage' ); + targetLangDir = $.uls.data.getDir( targetLangAttrib ); + } else { + targetLangAttrib = targetLangCode; + targetLangDir = this.$container.data( 'targetlangdir' ); + } + + $message = $( '<div>' ) + .addClass( 'row message tux-message-item ' + status ) + .append( + $( '<div>' ) + .addClass( 'eight columns tux-list-message' ) + .append( + $( '<span>' ) + .addClass( 'tux-list-source' ) + .attr( { + lang: sourceLangCode, + dir: sourceLangDir + } ) + .text( message.definition ), + // Bidirectional isolation. + // This should be removed some day when proper + // unicode-bidi: isolate + // is supported everywhere + $( '<span>' ) + .html( $( 'body' ).hasClass( 'rtl' ) ? '‏' : '‎' ), + $( '<span>' ) + .addClass( 'tux-list-translation' ) + .attr( { + lang: targetLangAttrib, + dir: targetLangDir + } ) + .text( message.translation || '' ) + ), + $( '<div>' ) + .addClass( 'two columns tux-list-status text-center' ) + .append( + $( '<span>' ) + .addClass( statusClass ) + .text( statusMsg ? mw.msg( statusMsg ) : '' ) + ), + $( '<div>' ) + .addClass( 'two column tux-list-edit text-right' ) + .append( + $( '<a>' ) + .attr( { + title: mw.msg( 'translate-edit-title', message.key ), + href: mw.util.getUrl( message.title, { action: 'edit' } ) + } ) + .text( mw.msg( 'tux-edit' ) ) + ) + ); + + $messageWrapper.append( $message ); + this.$container.append( $messageWrapper ); + + // Attach translate editor to the message + $messageWrapper.translateeditor( { + message: message + } ); + }, + + /** + * Add a message to the message table for proofreading. + * + * @param {Object} message + */ + addProofread: function ( message ) { + var $message, $icon; + + $message = $( '<div>' ) + .addClass( 'row tux-message tux-message-proofread' ); + + this.$container.append( $message ); + $message.proofread( { + message: message, + sourcelangcode: this.$container.data( 'sourcelangcode' ), + targetlangcode: this.$container.data( 'targetlangcode' ) + } ); + + $icon = $message.find( '.tux-proofread-action' ); + if ( $icon.length === 0 ) { + return; + } + + // Add autotooltip to first available proofread action icon + if ( this.firstProofreadTipShown ) { + return; + } + this.firstProofreadTipShown = true; + $icon.addClass( 'autotooltip' ); + + mw.loader.using( 'oojs-ui-core' ).done( function () { + var tooltip = new OO.ui.PopupWidget( { + padded: true, + align: 'center', + width: 250, + classes: [ 'translate-tooltip' ], + $content: $( '<p>' ).text( $icon.prop( 'title' ) ) + } ); + + setTimeout( function () { + var offset, $icon = $( '.autotooltip:visible' ); + if ( !$icon.length ) { + return; + } + + offset = $icon.offset(); + tooltip.$element.appendTo( 'body' ); + tooltip.toggle( true ).toggleClipping( false ).togglePositioning( false ); + tooltip.$element.css( { + top: offset.top + $icon.outerHeight() + 5, + left: offset.left + $icon.outerWidth() - tooltip.$element.width() / 2 - 15 + } ); + + setTimeout( function () { + tooltip.$element.remove(); + }, 4000 ); + }, 1000 ); + } ); + }, + + /** + * Add a message to the message table for wiki page mode. + * + * @param {Object} message + */ + addPageModeMessage: function ( message ) { + var $message; + + $message = $( '<div>' ) + .addClass( 'row tux-message tux-message-pagemode' ); + + this.$container.append( $message ); + $message.pagemode( { + message: message, + sourcelangcode: this.$container.data( 'sourcelangcode' ), + targetlangcode: this.$container.data( 'targetlangcode' ) + } ); + }, + + /** + * Search the message filter + * + * @param {string} query + */ + search: function ( query ) { + var $note, $button, $result, + resultCount = 0, + matcher = new RegExp( '(^|\\s|\\b)' + escapeRegex( query ), 'gi' ); + + this.$container.find( itemsClass[ this.mode ] ).each( function () { + var $message = $( this ), + message = ( $message.data( 'translateeditor' ) || + $message.data( 'pagemode' ) || + $message.data( 'proofread' ) ).message; + + if ( matcher.test( message.definition ) || matcher.test( message.translation ) ) { + $message.removeClass( 'hide' ); + resultCount++; + } else { + $message.addClass( 'hide' ); + } + } ); + + $result = this.$container.find( '.tux-message-filter-result' ); + if ( !$result.length ) { + $note = $( '<div>' ) + .addClass( 'advanced-search' ); + + $button = $( '<button>' ) + .addClass( 'mw-ui-button' ) + .text( mw.msg( 'tux-message-filter-advanced-button' ) ); + + $result = $( '<div>' ) + .addClass( 'tux-message-filter-result' ) + .append( $note, $button ); + + this.$container.prepend( $result ); + } + + if ( !query ) { + $result.addClass( 'hide' ); + } else { + $result.removeClass( 'hide' ) + .find( '.advanced-search' ) + .text( mw.msg( 'tux-message-filter-result', resultCount, query ) ); + $result.find( 'button' ).on( 'click', function () { + window.location.href = mw.util.getUrl( 'Special:SearchTranslations', { query: query } ); + } ); + } + + this.updateLastMessage(); + + // Trigger a scroll event for the window to make sure all floating toolbars + // are in their position. + $( window ).trigger( 'scroll' ); + }, + + resize: function () { + var actualWidth = 0; + + // Calculate the total width required for the filters + $( '.row.tux-message-selector > li' ).each( function () { + actualWidth += $( this ).outerWidth( true ); + } ); + + // Grid row has a min width. After that scrollbars will appear. + // We are checking whether the message table is wider than the current grid row width. + if ( actualWidth >= parseInt( $( '.nine.columns' ).width(), 10 ) ) { + $( '.tux-message-selector .more ul' ) // Overflow menu + .prepend( $( '.row.tux-message-selector > li.column:last' ).prev() ); + + // See if more items to be pushed to the overflow menu + this.resize(); + } + }, + + /** + * Start loading messages again with new settings. + * + * @param {Object} changes + */ + changeSettings: function ( changes ) { + // Clear current messages + this.clear(); + this.settings = $.extend( this.settings, changes ); + + if ( this.initialized === false ) { + this.switchMode( this.mode ); + } + + // Reset the number of messages remaining + this.$loaderInfo.text( + mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) ) + ); + + // Reset the statsbar + this.$statsBar + .empty() + .removeData() + .languagestatsbar( { + language: this.settings.language, + group: this.settings.group + } ); + + this.initialized = true; + // Reset other info and make visible + this.$loader + .removeData( 'offset' ) + .removeAttr( 'data-offset' ) + .removeClass( 'hide' ); + + if ( changes.offset ) { + this.$loader.data( 'offset', changes.offset ); + } + + this.$header.removeClass( 'hide' ); + this.$actionBar.removeClass( 'hide' ); + + // Start loading messages + this.load( changes.limit ); + }, + + /** + * @param {number} [limit] Only load this many messages and then stop even if there is more. + */ + load: function ( limit ) { + var remaining, + query, + self = this, + offset = this.$loader.data( 'offset' ), + pageSize = limit || this.$loader.data( 'pagesize' ); + + if ( offset === -1 ) { + return; + } + + if ( this.loading ) { + // Avoid duplicate loading - the offset will be wrong and it will result + // in duplicate messages shown in the page + return; + } + + this.loading = true; + this.$loaderIcon.removeClass( 'tux-loading-indicator--stopped' ); + + mw.translate.getMessages( + this.settings.group, + this.settings.language, + offset, + pageSize, + this.settings.filter + ).done( function ( result ) { + var messages = result.query.messagecollection, + state, i; + + if ( !self.loading ) { + // reject. This was cancelled. + return; + } + + if ( result.warnings ) { + for ( i = 0; i !== result.warnings.length; i++ ) { + if ( result.warnings[ i ].code === 'translate-language-disabled-source' ) { + self.handleLoadErrors( [ result.warnings[ i ] ] ); + break; + } + } + return; + } + + if ( messages.length === 0 ) { + // And this is the first load for the filter... + if ( self.$container.children().length === 0 ) { + self.displayEmptyListHelp(); + } + } + + $.each( messages, function ( index, message ) { + message.group = self.settings.group; + self.add( message ); + self.messages.push( message ); + + if ( index === 0 && self.mode === 'translate' ) { + $( '.tux-message:first' ).data( 'translateeditor' ).init(); + } + } ); + + state = result.query.metadata && result.query.metadata.state; + $( '.tux-workflow' ).workflowselector( + self.settings.group, + self.settings.language, + state + ).removeClass( 'hide' ); + + // Dynamically loaded messages should pass the search filter if present. + query = $( '.tux-message-filter-box' ).val(); + + if ( query ) { + self.search( query ); + } + + if ( result[ 'query-continue' ] === undefined || limit ) { + // End of messages + self.$loader.data( 'offset', -1 ) + .addClass( 'hide' ); + + // Helpfully open the first message in show mode + // TODO: Refactor to avoid direct DOM access + $( '.tux-message-item' ).first().click(); + } else { + self.$loader.data( 'offset', result[ 'query-continue' ].messagecollection.mcoffset ); + + remaining = result.query.metadata.remaining; + + self.$loaderInfo.text( + mw.msg( 'tux-messagetable-more-messages', remaining ) + ); + + // Make sure the floating toolbars are visible without the need for scroll + $( window ).trigger( 'scroll' ); + } + + self.updateHideOwnInProofreadingToggleVisibility(); + self.updateLastMessage(); + } ).fail( function ( errorCode, response ) { + self.handleLoadErrors( response.errors, errorCode ); + } ).always( function () { + self.$loaderIcon.addClass( 'tux-loading-indicator--stopped' ); + self.loading = false; + } ); + }, + + updateLastMessage: function () { + var $messages = this.$container.find( itemsClass[ this.mode ] ); + + // If a message was previously marked as "last", restore it to normal state + $messages.filter( '.last-message' ).removeClass( 'last-message' ); + + // At the class to the current last shown message + $messages + .not( '.hide' ) + .last() + .addClass( 'last-message' ); + }, + + /** + * Creates a uniformly styled button for different actions, + * shown when there are no messages to display. + * + * @param {string} labelMsg A message key for the button label. + * @param {Function} callback A callback for clicking the button. + * @return {jQuery} A button element. + */ + otherActionButton: function ( labelMsg, callback ) { + return $( '<button>' ) + .addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' ) + .text( mw.msg( labelMsg ) ) + .on( 'click', callback ); + }, + + /** + * Enables own message hiding in proofread mode. + * + * @param {boolean} enabled + */ + setHideOwnInProofreading: function ( enabled ) { + if ( enabled ) { + this.$container.addClass( 'tux-hide-own' ); + } else { + this.$container.removeClass( 'tux-hide-own' ); + } + }, + + updateHideOwnInProofreadingToggleVisibility: function () { + if ( this.$container.find( '.tux-message-proofread.own-translation' ).length ) { + this.$proofreadOwnTranslations.removeClass( 'hide' ); + } else { + this.$proofreadOwnTranslations.addClass( 'hide' ); + } + }, + + /** + * If the user selection doesn't show anything, + * give some pointers to other things to do. + */ + displayEmptyListHelp: function () { + var messageTable = this, + // @todo Ugly! This should be provided somehow + selectedTab = $( '.tux-message-selector .selected' ).data( 'title' ), + $wrap = $( '<div>' ).addClass( 'tux-empty-list' ), + $emptyListHeader = $( '<div>' ).addClass( 'tux-empty-list-header' ), + $guide = $( '<div>' ).addClass( 'tux-empty-list-guide' ), + $actions = $( '<div>' ).addClass( 'tux-empty-list-actions' ); + + if ( messageTable.mode === 'proofread' ) { + if ( selectedTab === 'all' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-no-messages-to-display' ) ); + $guide.append( + $( '<p>' ) + .text( mw.msg( 'tux-empty-there-are-optional' ) ), + $( '<a>' ) + .attr( 'href', '#' ) + .text( mw.msg( 'tux-empty-show-optional-messages' ) ) + .on( 'click', function ( e ) { + $( '#tux-option-optional' ).click(); + e.preventDefault(); + } ) + ); + } else if ( selectedTab === 'outdated' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-no-outdated-messages' ) ); + $guide.text( mw.msg( 'tux-empty-list-other-guide' ) ); + $actions.append( messageTable.otherActionButton( + 'tux-empty-list-other-action', + function () { + $( '.tux-tab-unproofread' ).click(); + // @todo untranslated + } ) + ); + // @todo View all + } else if ( selectedTab === 'translated' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-nothing-to-proofread' ) ); + $guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) ); + $actions.append( messageTable.otherActionButton( + 'tux-empty-list-translated-action', + function () { + messageTable.switchMode( 'translate' ); + } ) + ); + } else if ( selectedTab === 'unproofread' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-nothing-new-to-proofread' ) ); + $guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) ); + $actions.append( messageTable.otherActionButton( + 'tux-empty-you-can-review-already-proofread', + function () { + $( '.tux-tab-translated' ).click(); + } ) + ); + } + } else { + if ( selectedTab === 'all' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-list-all' ) ); + $guide.text( mw.msg( 'tux-empty-list-all-guide' ) ); + } else if ( selectedTab === 'translated' ) { + $emptyListHeader.text( mw.msg( 'tux-empty-list-translated' ) ); + $guide.text( mw.msg( 'tux-empty-list-translated-guide' ) ); + $actions.append( messageTable.otherActionButton( + 'tux-empty-list-translated-action', + function () { + mw.translate.changeFilter( $( '.tux-tab-untranslated' ).click() ); + } ) + ); + } else { + $emptyListHeader.text( mw.msg( 'tux-empty-list-other' ) ); + + if ( mw.translate.canProofread() ) { + $guide.text( mw.msg( 'tux-empty-list-other-guide' ) ); + $actions.append( messageTable.otherActionButton( + 'tux-empty-list-other-action', + function () { + messageTable.switchMode( 'proofread' ); + } ) + ); + } + + $actions.append( $( '<a>' ) + .text( mw.msg( 'tux-empty-list-other-link' ) ) + .click( function () { + $( '.tux-tab-all' ).click(); + } ) + ); + } + } + + $wrap.append( $emptyListHeader, $guide, $actions ); + this.$container.append( $wrap ); + }, + + /** + * Switch the message table mode + * + * @param {string} mode The message table mode to switch to: translate, page or proofread + */ + switchMode: function ( mode ) { + var messageTable = this, + filter = this.settings.filter, + userId = mw.config.get( 'wgUserId' ), + $tuxTabUntranslated, + $tuxTabUnproofread, + $hideTranslatedButton; + + messageTable.$actionBar.find( '.tux-view-switcher .down' ).removeClass( 'down' ); + if ( mode === 'translate' ) { + messageTable.$actionBar.find( '.translate-mode-button' ).addClass( 'down' ); + } + if ( mode === 'proofread' ) { + messageTable.$actionBar.find( '.proofread-mode-button' ).addClass( 'down' ); + } + if ( mode === 'page' ) { + messageTable.$actionBar.find( '.page-mode-button' ).addClass( 'down' ); + } + + messageTable.firstProofreadTipShown = false; + + messageTable.mode = mode; + mw.translate.changeUrl( { action: messageTable.mode } ); + + // Emulate clear without clearing loaded messages + messageTable.$container.empty(); + $( '.translate-tooltip' ).remove(); + + $tuxTabUntranslated = $( '.tux-message-selector > .tux-tab-untranslated' ); + $tuxTabUnproofread = $( '.tux-message-selector > .tux-tab-unproofread' ); + $hideTranslatedButton = messageTable.$actionBar.find( '.tux-editor-clear-translated' ); + + if ( messageTable.mode === 'proofread' ) { + $tuxTabUntranslated.addClass( 'hide' ); + $tuxTabUnproofread.removeClass( 'hide' ); + + // Fix the filter if it is untranslated. Untranslated does not make sense + // for proofread mode. Keep the filter if it is not 'untranslated' + if ( !filter || filter.indexOf( '!translated' ) >= 0 ) { + messageTable.messages = []; + // default filter for proofread mode + mw.translate.changeFilter( 'translated|!reviewer:' + userId + + '|!last-translator:' + userId ); + $tuxTabUnproofread.addClass( 'selected' ); + // Own translations are not present in proofread + unreviewed mode + } + + $hideTranslatedButton.addClass( 'hide' ); + } else { + $tuxTabUntranslated.removeClass( 'hide' ); + $tuxTabUnproofread.addClass( 'hide' ); + + if ( filter.indexOf( '!translated' ) > -1 ) { + $hideTranslatedButton.removeClass( 'hide' ); + } + + if ( filter && filter.indexOf( '!last-translator' ) >= 0 ) { + messageTable.messages = []; + // default filter for translate mode + mw.translate.changeFilter( '!translated' ); + $tuxTabUntranslated.addClass( 'selected' ); + } + } + + if ( messageTable.messages.length ) { + $.each( messageTable.messages, function ( index, message ) { + messageTable.add( message ); + } ); + } else if ( messageTable.initialized ) { + messageTable.displayEmptyListHelp(); + } + + this.$loaderInfo.text( + mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) ) + ); + + messageTable.updateHideOwnInProofreadingToggleVisibility(); + messageTable.updateLastMessage(); + }, + + /** + * The scroll handler + */ + scroll: function () { + var $window, + isActionBarFloating, + needsTableHeaderFloat, needsTableHeaderStick, + needsActionBarFloat, needsActionBarStick, + windowScrollTop, windowScrollBottom, + messageTableRelativePos, + messageListOffset, + messageListHeight, messageListWidth, + messageListTop, messageListBottom; + + $window = $( window ); + + windowScrollTop = $window.scrollTop(); + windowScrollBottom = windowScrollTop + $window.height(); + messageListOffset = this.$container.offset(); + messageListHeight = this.$container.height(); + messageListTop = messageListOffset.top; + messageListBottom = messageListTop + messageListHeight; + messageListWidth = this.$container.width(); + + // Header: + messageTableRelativePos = messageListTop - this.$header.height() - windowScrollTop; + needsTableHeaderFloat = messageTableRelativePos + 10 < 0; + needsTableHeaderStick = messageTableRelativePos - 10 >= 0; + if ( needsTableHeaderFloat ) { + this.$header.addClass( 'floating' ).width( messageListWidth ); + } else if ( needsTableHeaderStick ) { + // Let the element change width automatically again + this.$header.removeClass( 'floating' ).css( 'width', '' ); + } + + // Action bar: + isActionBarFloating = this.$actionBar.hasClass( 'floating' ); + needsActionBarFloat = windowScrollBottom < messageListBottom; + needsActionBarStick = windowScrollBottom > ( messageListBottom + this.$actionBar.height() ); + + if ( !isActionBarFloating && needsActionBarFloat ) { + this.$actionBar.addClass( 'floating' ).width( messageListWidth ); + } else if ( isActionBarFloating && needsActionBarStick ) { + // Let the element change width automatically again + this.$actionBar.removeClass( 'floating' ).css( 'width', '' ); + } else if ( isActionBarFloating && needsActionBarFloat ) { + this.$actionBar.width( messageListWidth ); + } + }, + + /** + * Handles errors encountered during the loading state. + * Displays the errors and updates the state of the table. + * + * @param {Array} errors + * @param {string} errorCode + */ + handleLoadErrors: function ( errors, errorCode ) { + var $warningContainer = $( '.tux-editor-header .group-warning' ); + + if ( errors ) { + $.map( errors, function ( error ) { + $warningContainer.append( error[ '*' ] ); + } ); + } else { + $warningContainer.text( mw.msg( 'api-error-unknownerror', errorCode ) ); + } + + $( '.tux-workflow' ).addClass( 'hide' ); + this.$loader.data( 'offset', -1 ).addClass( 'hide' ); + this.$actionBar.addClass( 'hide' ); + this.$header.addClass( 'hide' ); + } + }; + + /* + * messagetable PLUGIN DEFINITION + */ + + $.fn.messagetable = function ( options ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'messagetable' ); + + if ( !data ) { + $this.data( 'messagetable', ( data = new MessageTable( this, options ) ) ); + } + + if ( typeof options === 'string' ) { + data[ options ].call( $this ); + } + } ); + }; + + $.fn.messagetable.Constructor = MessageTable; + + $.fn.messagetable.defaults = { + mode: new mw.Uri().query.action || 'translate' + }; + + /** + * Escape the search query for regex match. + * + * @param {string} value A search string to be escaped. + * @return {string} Escaped string that is safe to use for a search. + */ + function escapeRegex( value ) { + return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' ); + } + + function isLoaderVisible( $loader ) { + var viewportBottom, elementTop, + $window = $( window ); + + viewportBottom = ( window.innerHeight ? window.innerHeight : $window.height() ) + + $window.scrollTop(); + + elementTop = $loader.offset().top; + + // Start already if user is reaching close to the bottom + return elementTop - viewportBottom < 200; + } + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.multiselectautocomplete.js b/www/wiki/extensions/Translate/resources/js/ext.translate.multiselectautocomplete.js new file mode 100644 index 00000000..6d6a8201 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.multiselectautocomplete.js @@ -0,0 +1,95 @@ +/*! + * @author Santhosh Thottingal + * jQuery autocomplete based multiple selector for input box. + * Autocompleted values will be available in input filed as comma separated values. + * The values for autocompletion is from the language selector in this case. + * The input field is created in PHP code. + * Credits: https://jqueryui.com/autocomplete/#multiple + */ +$( function () { + 'use strict'; + + /* eslint-disable no-underscore-dangle */ + + $.widget( 'ui.multiselectautocomplete', { + options: { + inputbox: null // a jQuery selector for the input box where selections are written. + // TODO can have more options. + }, + _create: function () { + var self, select, options, input; + + self = this; + select = this.element.hide(); + options = this.options; + + function split( val ) { + return val.split( /,\s*/ ); + } + + input = this.input = $( options.inputbox ).autocomplete( { + delay: 0, + minLength: 0, + source: function ( request, response ) { + var term, matcher; + + term = split( request.term ).pop(); + matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), 'i' ); + + response( select.children( 'option' ).map( function () { + var text = $( this ).html(), + value = $( this ).val(), + term = split( request.term ).pop(); + + if ( this.value && ( !request.term || matcher.test( text ) ) ) { + if ( term.trim() !== '' ) { + text = text.replace( + new RegExp( + '(?![^&;]+;)(?!<[^<>]*)(' + + $.ui.autocomplete.escapeRegex( term ) + + ')(?![^<>]*>)(?![^&;]+;)', 'gi' + ), '<strong>$1</strong>' ); + } + + return { + label: text, + value: value, + option: this + }; + } + } ) ); + }, + select: function ( event, ui ) { + var terms; + + ui.item.option.selected = true; + self._trigger( 'selected', event, { + item: ui.item.option + } ); + terms = split( $( this ).val() ); + // remove the current input + terms.pop(); + // add the selected item + terms.push( ui.item.value ); + // add placeholder to get the comma-and-space at the end + terms.push( '' ); + $( this ).val( terms.join( ', ' ) ); + return false; + } + } ); + + input.data( 'autocomplete' )._renderItem = function ( ul, item ) { + return $( '<li>' ) + .data( 'item.autocomplete', item ) + .append( '<a>' + item.label + '</a>' ) + .appendTo( ul ); + }; + }, // End of _create + + destroy: function () { + this.input.remove(); + this.element.show(); + $.Widget.prototype.destroy.call( this ); + } + } ); +} ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.navitoggle.js b/www/wiki/extensions/Translate/resources/js/ext.translate.navitoggle.js new file mode 100644 index 00000000..dadaee7c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.navitoggle.js @@ -0,0 +1,41 @@ +/*! + * Introduces a toggle icon than can be used to hide navigation menu in vector + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ +( function () { + 'use strict'; + + var $body = $( 'body' ); + + if ( $body.width() < 1000 || mw.storage.get( 'translate-navitoggle' ) === '1' ) { + $body.addClass( 'tux-navi-collapsed' ); + } + + $( function () { + var $miniLogo, $toggle, rtl, delim; + + rtl = $body.hasClass( 'rtl' ); + delim = rtl ? + $( '#mw-head-base' ).css( 'margin-right' ) : + $( '#mw-head-base' ).css( 'margin-left' ); + + $miniLogo = $( '#p-logo' ) + .clone() + .removeAttr( 'id' ) + .addClass( 'tux-navi-minilogo' ); + + $toggle = $( '<div>' ) + .addClass( 'tux-navitoggle' ) + .css( rtl ? 'right' : 'left', delim ) + .click( function () { + $body.toggleClass( 'tux-navi-collapsed' ); + mw.storage.set( + 'translate-navitoggle', + String( Number( $body.hasClass( 'tux-navi-collapsed' ) ) ) + ); + } ); + + $body.append( $miniLogo, $toggle ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.pagemode.js b/www/wiki/extensions/Translate/resources/js/ext.translate.pagemode.js new file mode 100644 index 00000000..14d7a710 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.pagemode.js @@ -0,0 +1,136 @@ +( function () { + 'use strict'; + /** + * Page mode plugin + * + * Prepare the page mode UI with all the required actions + * for a translation unit (message). + * This is mainly used with the messagetable plugin in page mode, + * but it is independent of messagetable. + * Example usage: + * + * $( 'div.pagemode' ).pagemode( { + * message: messageObject, // Mandatory message object + * sourcelangcode: 'en', // Mandatory source language code + * targetlangcode: 'hi' // Mandatory target language code + * } ); + * + * @param {Element} element + * @param {Object} options + * @param {Object} options.message + * @param {string} options.sourcelangcode Language code. + * @param {string} options.targetlangcode Language code. + */ + function PageMode( element, options ) { + this.$message = $( element ); + this.options = options; + this.message = this.options.message; + this.init(); + this.listen(); + } + + PageMode.prototype = { + + /** + * Initialize the plugin + */ + init: function () { + var pagemode = this; + + this.message.proofreadable = false; + + this.render(); + + pagemode.$message.translateeditor( { + message: pagemode.message, + beforeSave: function ( translation ) { + pagemode.$message.find( '.tux-pagemode-translation' ) + .html( mw.translate.formatMessageGently( translation || '', pagemode.message.key ) ) + .addClass( 'highlight' ); + }, + onSave: function ( translation ) { + pagemode.$message.find( '.tux-pagemode-translation' ) + .removeClass( 'highlight' ); + pagemode.message.translation = translation; + + pagemode.$message.find( '.tux-pagemode-status' ) + .removeClass( 'translated fuzzy proofread untranslated' ) + .addClass( pagemode.message.properties.status ); + } + } ); + + }, + + render: function () { + var targetLangAttrib, targetLangDir, + sourceLangDir = $.uls.data.getDir( this.options.sourcelangcode ); + + if ( this.options.targetlangcode === + mw.config.get( 'wgTranslateDocumentationLanguageCode' ) + ) { + targetLangAttrib = mw.config.get( 'wgContentLanguage' ); + } else { + targetLangAttrib = this.options.targetlangcode; + } + + targetLangDir = $.uls.data.getDir( targetLangAttrib ); + + this.$message.append( + $( '<div>' ) + .addClass( 'row tux-message-item-compact message ' + this.message.properties.status ) + .append( + $( '<div>' ) + .addClass( 'one column tux-pagemode-status ' + this.message.properties.status ), + $( '<div>' ) + .addClass( 'five columns tux-pagemode-source' ) + .attr( { + lang: this.options.sourcelangcode, + dir: sourceLangDir + } ) + .html( mw.translate.formatMessageGently( this.message.definition, this.message.key ) ), + $( '<div>' ) + .addClass( 'five columns tux-pagemode-translation' ) + .attr( { + lang: targetLangAttrib, + dir: targetLangDir + } ) + .html( mw.translate.formatMessageGently( this.message.translation || '', this.message.key ) ), + $( '<div>' ) + .attr( 'title', mw.msg( 'translate-edit-title', this.message.key ) ) + .addClass( 'tux-pagemode-edit' ) + ) + ).addClass( this.message.properties.status ); + }, + + /** + * Attach event listeners + */ + listen: function () { + var pagemode = this; + + this.$message.children( '.message' ).on( 'click', function ( e ) { + pagemode.$message.data( 'translateeditor' ).show(); + e.preventDefault(); + } ); + } + }; + + /* + * pagemode PLUGIN DEFINITION + */ + $.fn.pagemode = function ( options ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'pagemode' ); + + if ( !data ) { + $this.data( 'pagemode', + ( data = new PageMode( this, options ) ) + ); + } + + } ); + }; + + $.fn.pagemode.Constructor = PageMode; +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.pagetranslation.uls.js b/www/wiki/extensions/Translate/resources/js/ext.translate.pagetranslation.uls.js new file mode 100644 index 00000000..00c67e9e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.pagetranslation.uls.js @@ -0,0 +1,15 @@ +( function () { + 'use strict'; + + mw.uls.changeLanguage = function ( language ) { + var page; + + page = 'Special:MyLanguage/' + mw.config.get( 'wgPageName' ); + + if ( mw.config.get( 'wgTranslatePageTranslation' ) === 'translation' ) { + page = page.replace( /\/[^/]+$/, '' ); + } + + location.href = mw.util.getUrl( page, { setlang: language } ); + }; +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.parsers.js b/www/wiki/extensions/Translate/resources/js/ext.translate.parsers.js new file mode 100644 index 00000000..afcf0c8e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.parsers.js @@ -0,0 +1,81 @@ +/*! + * A set of simple tools for partial parsing and formatting of translatable + * messages. + * + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +( function () { + 'use strict'; + + mw.translate = mw.translate || {}; + mw.translate = $.extend( mw.translate, { + /** + * Formats some common wikitext elements. + * + * @param {string} text Message text + * @param {string} [key] Message key + * @return {string} Formatted text in html + */ + formatMessageGently: function ( text, key ) { + var externals, + protocols = mw.config.get( 'wgUrlProtocols' ); + + // Try to keep simple. + text = $( '<div>' ).text( text ).html(); + + // Hack for page translation page titles + if ( text && key && key.match( /\/Page_display_title$/ ) ) { + text = '=' + text + '='; + } + + text = text.replace( /^(=+)(.*?)(=+)/, function ( match, p1, p2, p3 ) { + var len = Math.min( p1.length, p3.length, 6 ); + return $( '<div>' ).append( $( '<h' + len + '>' ).html( p2 ) ).html(); + } ); + + text = text.replace( /(^\*.*(\n|$))+/gm, function ( match ) { + match = match.replace( /^\*(.*)/gm, function ( match, p1 ) { + return $( '<div>' ).append( $( '<li>' ).html( p1 ) ).html(); + } ); + return $( '<div>' ).append( $( '<ul>' ).html( match ) ).html(); + } ); + + text = text.replace( /(^#.*(\n|$))+/gm, function ( match ) { + match = match.replace( /^#(.*)/gm, function ( match, p1 ) { + return $( '<div>' ).append( $( '<li>' ).html( p1 ) ).html(); + } ); + return $( '<div>' ).append( $( '<ol>' ).html( match ) ).html(); + } ); + + text = text.replace( /\[\[([^\]|]+?)\|(.+?)\]\]/g, function ( match, p1, p2 ) { + var link = $( '<a>' ).html( p2 ).prop( 'href', mw.util.getUrl( p1 ) ); + return $( '<div>' ).append( link ).html(); + } ); + + text = text.replace( /\[\[(.+?)\]\]/g, function ( match, p1 ) { + var link = $( '<a>' ).html( p1 ).prop( 'href', mw.util.getUrl( p1 ) ); + return $( '<div>' ).append( link ).html(); + } ); + + externals = new RegExp( '\\[((' + protocols + ')[^ ]+) (.+?)\\]', 'g' ); + text = text.replace( externals, function ( match, p1, p2, p3 ) { + var link = $( '<a>' ).html( p3 ).prop( 'href', p1 ); + return $( '<div>' ).append( link ).html(); + } ); + + text = text.replace( /'''(.+?)'''/g, function ( match, p1 ) { + return $( '<div>' ).append( $( '<strong>' ).html( p1 ) ).html(); + } ); + + text = text.replace( /''(.+?)''/g, function ( match, p1 ) { + return $( '<div>' ).append( $( '<em>' ).html( p1 ) ).html(); + } ); + + text = text.replace( /\n\n/gm, '<br />' ); + return text; + } + } ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.proofread.js b/www/wiki/extensions/Translate/resources/js/ext.translate.proofread.js new file mode 100644 index 00000000..8ab3bbe5 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.proofread.js @@ -0,0 +1,282 @@ +( function () { + 'use strict'; + + /** + * Proofread Plugin + * Prepare a proofread UI with all the required actions + * for a translation unit (message). + * This is mainly used with the messagetable plugin in proofread mode, + * but it is independent of messagetable. + * Example usage: + * + * $( 'div.proofread' ).proofread( { + * message: messageObject, // Mandatory message object + * sourcelangcode: 'en', // Mandatory source language code + * targetlangcode: 'hi' // Mandatory target language code + * } ); + * + * @param {Element} element + * @param {Object} options + * @param {Object} options.message + * @param {string} options.sourcelangcode Language code. + * @param {string} options.targetlangcode Language code. + */ + function Proofread( element, options ) { + this.$message = $( element ); + this.options = options; + this.message = this.options.message; + this.init(); + this.listen(); + } + + Proofread.prototype = { + + /** + * Initialize the plugin + */ + init: function () { + var proofread = this; + + this.render(); + + // No review before translating. + if ( !this.message.translation ) { + this.disableProofread(); + } + + // No review for fuzzy messages. + if ( this.message.properties.status === 'fuzzy' ) { + this.disableProofread(); + } + + if ( !mw.translate.canProofread() ) { + this.disableProofread(); + } + + proofread.$message.translateeditor( { + message: proofread.message, + onSave: function ( translation ) { + proofread.$message.find( '.tux-proofread-translation' ) + .text( translation ); + proofread.message.translation = translation; + proofread.markSelfTranslation(); + + proofread.$message.find( '.tux-proofread-status' ) + .removeClass( 'translated fuzzy proofread untranslated' ) + .addClass( proofread.message.properties.status ); + } + } ); + + }, + + render: function () { + var targetLangCode, targetLangDir, targetLangAttrib, + sourceLangCode, sourceLangDir, + $proofreadAction, $proofreadEdit, userId, reviewers, otherReviewers, + translatedBySelf, proofreadBySelf; + + // List of all reviewers + reviewers = this.message.properties.reviewers || []; + // The id of the current user, converted to string as the are in reviewers + userId = String( mw.config.get( 'wgUserId' ) ); + // List of all reviewers excluding the current user. + otherReviewers = reviewers.filter( function ( element ) { + return element !== userId; + } ); + /* Whether the current user if the last translator of this message. + * Accepting own translations is prohibited. */ + translatedBySelf = ( this.message.properties[ 'last-translator-text' ] === mw.user.getName() ); + proofreadBySelf = reviewers.indexOf( userId ) > -1; + + sourceLangCode = this.options.sourcelangcode; + sourceLangDir = $.uls.data.getDir( sourceLangCode ); + targetLangCode = this.options.targetlangcode; + targetLangDir = $.uls.data.getDir( targetLangCode ); + + $proofreadAction = $( '<div>' ) + .attr( 'title', mw.msg( 'tux-proofread-action-tooltip' ) ) + .addClass( + 'tux-proofread-action ' + this.message.properties.status + ' ' + ( proofreadBySelf ? 'accepted' : '' ) + ); + + $proofreadEdit = $( '<div>' ) + .addClass( 'tux-proofread-edit' ) + .append( $( '<span>' ) + .addClass( 'tux-proofread-edit-label hide' ) + .text( mw.msg( 'tux-proofread-edit-label' ) ) + ) + .on( 'mouseover', function () { + $( this ).find( '.tux-proofread-edit-label' ).removeClass( 'hide' ); + } ) + .on( 'mouseout', function () { + $( this ).find( '.tux-proofread-edit-label' ).addClass( 'hide' ); + } ); + + if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + targetLangAttrib = mw.config.get( 'wgContentLanguage' ); + } else { + targetLangAttrib = targetLangCode; + } + targetLangDir = $.uls.data.getDir( targetLangAttrib ); + + this.$message.append( + $( '<div>' ) + .addClass( 'row tux-message-item-compact message' ) + .append( + $( '<div>' ) + .addClass( 'one column tux-proofread-status ' + this.message.properties.status ), + $( '<div>' ) + .addClass( 'five columns tux-proofread-source' ) + .attr( { + lang: sourceLangCode, + dir: sourceLangDir + } ) + .text( this.message.definition ), + $( '<div>' ) + .addClass( 'five columns tux-proofread-translation' ) + .attr( { + lang: targetLangAttrib, + dir: targetLangDir + } ) + .text( this.message.translation || '' ), + $( '<div>' ) + .addClass( 'tux-proofread-action-block one column' ) + .append( + $proofreadAction, + otherReviewers.length ? + $( '<div>' ) + .addClass( 'tux-proofread-count' ) + .data( 'reviewCount', reviewers.length ) // To update when accepting + .text( mw.language.convertNumber( reviewers.length ) ) : + $( [] ), + $proofreadEdit + ) + ) + ).addClass( this.message.properties.status ); + + if ( !translatedBySelf && !proofreadBySelf ) { + // This will get removed later if any of various other reasons prevent it + this.message.proofreadable = true; + this.message.proofreadAction = this.proofread.bind( this ); + } + + if ( translatedBySelf ) { + this.markSelfTranslation(); + } + + /* Here we need to check that there are reviewers in the first place + * before adding review markers */ + if ( reviewers.length && otherReviewers.length ) { + this.$message.addClass( 'proofread-by-others' ); + } + }, + + disableProofread: function () { + this.message.proofreadable = false; + this.$message.find( '.tux-proofread-action' ) + .remove(); + }, + + /** + * Mark the message self translated. + */ + markSelfTranslation: function () { + // Own translations cannot be reviewed, so disable proofread + this.disableProofread(); + if ( !this.$message.hasClass( 'own-translation' ) ) { + this.$message.addClass( 'own-translation' ) + .find( '.tux-proofread-action-block' ) + .append( $( '<div>' ) + .addClass( 'translated-by-self' ) + .attr( 'title', mw.msg( 'tux-proofread-translated-by-self' ) ) + ); + } + }, + /** + * Mark this message as proofread. + */ + proofread: function () { + var reviews, counter, params, + message = this.message, + $message = this.$message, + api = new mw.Api(); + + params = { + action: 'translationreview', + revision: this.message.properties.revision + }; + + if ( !mw.user.isAnon() ) { + params.assert = 'user'; + } + + api.postWithToken( 'csrf', params ).done( function () { + $message.find( '.tux-proofread-action' ) + .removeClass( 'tux-warning' ) // in case, it failed previously + .addClass( 'accepted' ); + + counter = $message.find( '.tux-proofread-count' ); + reviews = counter.data( 'reviewCount' ); + counter.text( mw.language.convertNumber( reviews + 1 ) ); + + // Update stats + $( '.tux-action-bar .tux-statsbar' ).trigger( + 'change', + [ 'proofread', message.properties.status ] + ); + + message.properties.status = 'proofread'; + + if ( mw.track ) { + mw.track( 'ext.translate.event.proofread', message ); + } + } ).fail( function ( errorCode ) { + $message.find( '.tux-proofread-action' ).addClass( 'tux-warning' ); + if ( errorCode === 'assertuserfailed' ) { + // eslint-disable-next-line no-alert + alert( mw.msg( 'tux-session-expired' ) ); + } + } ); + }, + + /** + * Attach event listeners + */ + listen: function () { + var proofread = this; + + this.$message.find( '.tux-proofread-action' ).on( 'click', function () { + proofread.proofread(); + return false; + } ); + + this.$message.find( '.tux-proofread-edit' ).on( 'click', function () { + // Make sure that the tooltip is hidden when going to the editor + $( '.translate-tooltip' ).remove(); + proofread.$message.data( 'translateeditor' ).show(); + + return false; + } ); + } + }; + + /* + * proofread PLUGIN DEFINITION + */ + $.fn.proofread = function ( options ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'proofread' ); + + if ( !data ) { + $this.data( 'proofread', + ( data = new Proofread( this, options ) ) + ); + } + + } ); + }; + + $.fn.proofread.Constructor = Proofread; + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.quickedit.js b/www/wiki/extensions/Translate/resources/js/ext.translate.quickedit.js new file mode 100644 index 00000000..26d73906 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.quickedit.js @@ -0,0 +1,402 @@ +/*! + * JavaScript that implements the Ajax translation interface, which was at the + * time of writing this probably the biggest usability problem in the extension. + * Most importantly, it speeds up translating and keeps the list of translatable + * messages open. It also allows multiple translation dialogs, for doing quick + * updates to other messages or documentation, or translating multiple languages + * simultaneously together with the "In other languages" display included in + * translation helpers and implemented by utils/TranslationhHelpers.php. + * The form itself is implemented by utils/TranslationEditPage.php, which is + * called from Special:Translate/editpage?page=Namespace:pagename. + * + * TODO list: + * * Instead of hc'd onscript, give them a class and use necessary triggers + * + * @author Niklas Laxström + * @license GPL-2.0+ + */ + +( function ( $, mw, autosize ) { + 'use strict'; + var dialogwidth = false, + preloads = {}; + + mw.translate = mw.translate || {}; + function MessageCheckUpdater( callback ) { + this.act = function () { + callback(); + delete this.timeoutID; + }; + + this.setup = function () { + var self = this; + + this.cancel(); + this.timeoutID = window.setTimeout( self.act, 1000 ); + }; + + this.cancel = function () { + if ( typeof this.timeoutID === 'number' ) { + window.clearTimeout( this.timeoutID ); + delete this.timeoutID; + } + }; + } + + /** + * This is JS port same method of TranslateUtils.php + */ + function convertWhiteSpaceToHTML( text ) { + return mw.html.escape( text ) + .replace( /^ /gm, ' ' ) + .replace( / $/gm, ' ' ) + .replace( / {2}/g, '  ' ) + .replace( /\n/g, '<br />' ); + } + + function addAccessKeys( dialog ) { + var buttons = { + a: '.mw-translate-save', + s: '.mw-translate-next', + d: '.mw-translate-skip', + h: '.mw-translate-history' + }; + + $.each( buttons, function ( key, selector ) { + $( selector ) + .val( function ( i, b ) { + return b.replace( / \(.\)$/, '' ); + } ) + .removeAttr( 'accesskey' ) + .attr( 'title', '' ); + + dialog.find( selector ) + .val( function ( i, b ) { + return b + ' (_)'.replace( '_', key ); + } ) + .attr( 'accesskey', key ) + .attr( 'title', '[' + mw.util.tooltipAccessKeyPrefix + key + ']' ); + } ); + } + + function registerFeatures( callbacks, form, page, group ) { + var $identical, textarea, checker; + + // Enable the collapsible element + $identical = $( '.mw-identical-title' ); + if ( $.isFunction( $identical.makeCollapsible ) ) { + $identical.makeCollapsible(); + } + + if ( mw.config.get( 'trlKeys' ) || $( '.tqe-inlineeditable' ).length ) { + if ( callbacks.next === undefined ) { + form.find( '.mw-translate-next, .mw-translate-skip' ).prop( 'disabled', true ); + } else { + form.find( '.mw-translate-next' ).click( function () { + if ( callbacks.next ) { + callbacks.next(); + } + } ); + form.find( '.mw-translate-skip' ).click( function () { + if ( callbacks.close ) { + callbacks.close(); + } + if ( callbacks.next ) { + callbacks.next(); + } + } ); + } + } else { + form.find( '.mw-translate-next, .mw-translate-skip' ) + .prop( 'disabled', true ) + .css( 'display', 'none' ); + } + form.find( '.mw-translate-close' ).click( function () { + if ( callbacks.close ) { + callbacks.close(); + } + } ); + + form.find( '.mw-translate-history' ).click( function () { + window.open( mw.util.getUrl( form.find( 'input[name=title]' ).val(), { action: 'history' } ) ); + return false; + } ); + + form.find( '.mw-translate-support, .mw-translate-askpermission' ).click( function () { + // Can use .data() only with 1.4.3 or newer + window.open( $( this ).attr( 'data-load-url' ) ); + return false; + } ); + + form.find( 'input, textarea' ).focus( function () { + addAccessKeys( form ); + } ); + + form.find( 'input#summary' ).focus( function () { + $( this ).css( 'width', '85%' ); + } ); + + textarea = form.find( '.mw-translate-edit-area' ); + textarea.css( 'display', 'block' ); + autosize( textarea ); + textarea[ 0 ].focus(); + + if ( form.find( '.mw-translate-messagechecks' ) ) { + checker = new MessageCheckUpdater( function () { + var url = mw.util.getUrl( 'Special:Translate/editpage', { + suggestions: 'checks', + page: page, + loadgroup: group + } ); + $.post( url, { translation: textarea.val() }, function ( mydata ) { + form.find( '.mw-translate-messagechecks' ).replaceWith( mydata ); + } ); + } ); + + textarea.keyup( function () { + checker.setup(); + } ); + } + + } + + mw.translate = $.extend( mw.translate, { + init: function () { + var $inlines, $first, title, group, prev; + + dialogwidth = $( window ).width() * 0.8; + $inlines = $( '.tqe-inlineeditable' ); + $inlines.dblclick( mw.translate.inlineEditor ); + + $first = $inlines.first(); + if ( $first.length ) { + title = $first.data( 'title' ); + group = $first.data( 'group' ); + mw.translate.loadEditor( null, title, group, $.noop ); + } + + prev = null; + $inlines.each( function () { + if ( prev ) { + prev.next = this; + } + prev = this; + } ); + }, + + openDialog: function ( page, group ) { + var id, dialogElement, dialog, callbacks; + + id = 'jsedit' + page.replace( /[^a-zA-Z0-9_]/g, '_' ); + dialogElement = $( '#' + id ); + + if ( dialogElement.size() > 0 ) { + dialogElement.dialog( 'option', 'position', 'top' ); + dialogElement.dialog( 'open' ); + return false; + } + + dialog = $( '<div>' ).attr( 'id', id ).appendTo( $( 'body' ) ); + + callbacks = {}; + callbacks.close = function () { + dialog.dialog( 'close' ); + }; + callbacks.next = function () { + mw.translate.openNext( page, group ); + }; + callbacks.success = function ( text ) { + var $td = $( '.tqe-inlineeditable' ).filter( function () { + return $( this ).data( 'title' ) === page.replace( '_', ' ' ); + } ); + $td + .html( convertWhiteSpaceToHTML( text ) ) + // T41233: hacky, but better than nothing. + // T130390: must be attr for IE/Edge. + .attr( 'dir', 'auto' ) + .removeClass( 'untranslated' ) + .addClass( 'justtranslated' ); + }; + mw.translate.openEditor( dialog, page, group, callbacks ); + + dialog.dialog( { + bgiframe: true, + width: dialogwidth, + title: page, + position: 'top', + resize: function () { + $( '#' + id + ' textarea' ).width( '100%' ); + }, + resizeStop: function () { + dialogwidth = $( '#' + id ).width(); + } + } ); + + return false; + }, + + loadEditor: function ( $target, page, group, callback ) { + var id, preload, url; + + // Try if it has been cached + id = 'preload-' + page.replace( /[^a-zA-Z0-9_]/g, '_' ); + preload = preloads[ id ]; + + if ( preload !== undefined ) { + if ( $target ) { + $target.html( preloads[ id ] ); + delete preloads[ id ]; + } + callback(); + return; + } + + // Load the editor into provided target or cache it locally + url = mw.util.getUrl( 'Special:Translate/editpage', { + suggestions: 'sync', + page: page, + loadgroup: group + } ); + if ( $target ) { + $target.load( url, callback ); + delete preloads[ id ]; + } else { + $.get( url, function ( data ) { + preloads[ id ] = data; + } ); + } + + }, + + openEditor: function ( element, page, group, callbacks ) { + var $target = $( element ), + spinner = $( '<div>' ).attr( 'class', 'mw-ajax-loader' ); + + $target.html( $( '<div>' ).attr( 'class', 'mw-ajax-dialog' ).html( spinner ) ); + + mw.translate.loadEditor( $target, page, group, function () { + var form; + + if ( callbacks.load ) { + callbacks.load( $target ); + } + + form = $target.find( 'form' ); + registerFeatures( callbacks, form, page, group ); + form.on( 'submit', function () { + mw.translateHooks.run( 'beforeSubmit', form ); + $( this ).ajaxSubmit( { + dataType: 'json', + success: function ( json ) { + mw.translateHooks.run( 'afterSubmit', form ); + if ( json.error ) { + if ( json.error.code === 'emptypage' ) { + window.alert( mw.msg( 'api-error-emptypage' ) ); + } else { + window.alert( json.error.info + ' (' + json.error.code + ')' ); + } + } else if ( json.edit.result === 'Failure' ) { + window.alert( mw.msg( 'translate-js-save-failed' ) ); + } else if ( json.edit.result === 'Success' ) { + if ( callbacks.close ) { + callbacks.close(); + } + if ( callbacks.success ) { + callbacks.success( form.find( '.mw-translate-edit-area' ).val() ); + } + } else { + window.alert( mw.msg( 'translate-js-save-failed' ) ); + } + } + } ); + return false; + } ); + } ); + }, + + openNext: function ( title, group ) { + var key, value, + messages = mw.config.get( 'trlKeys' ), + found = false; + + for ( key in messages ) { + if ( !messages.hasOwnProperty( key ) ) { + continue; + } + + value = messages[ key ]; + if ( found ) { + return mw.translate.openDialog( value, group ); + } else if ( value === title ) { + found = true; + } + } + window.alert( mw.msg( 'translate-js-nonext' ) ); + }, + + inlineEditor: function () { + var $this, current, page, group, next, callbacks, ntitle, ngroup, sel; + $this = $( this ); + + if ( $this.hasClass( 'tqe-editor-loaded' ) ) { + // Editor is open, do not replace it + return; + } + + current = $this.html(); + $this.addClass( 'tqe-editor-loaded' ); + + page = $this.data( 'title' ); + group = $this.data( 'group' ); + next = $( this.next ); + callbacks = {}; + + callbacks.success = function ( text ) { + // Update the cell value with the new translation + $this + .html( convertWhiteSpaceToHTML( text ) ) + // T41233: hacky, but better than nothing. + // T130390: must be attr for IE/Edge. + .attr( 'dir', 'auto' ) + .removeClass( 'untranslated' ) + .addClass( 'justtranslated' ); + }; + callbacks.close = function () { + $this.html( current ); + $this.removeClass( 'tqe-editor-loaded' ); + }; + callbacks.load = function ( editor ) { + var $header = $( '<div class="tqe-fakeheader"></div>' ); + $header.text( page ); + $header.append( '<input type=button class="mw-translate-close" value="X" />' ); + + $( editor ).find( 'form' ).prepend( $header ); + }; + if ( next.length ) { + callbacks.next = function () { + next.dblclick(); + }; + // Preload the next item + ntitle = next.data( 'title' ); + ngroup = next.data( 'group' ); + + mw.translate.loadEditor( null, ntitle, ngroup, $.noop ); + } + mw.translate.openEditor( $this, page, group, callbacks ); + + // Remove any text selection caused by double clicking + sel = window.getSelection ? window.getSelection() : document.selection; + + if ( sel ) { + if ( sel.removeAllRanges ) { + sel.removeAllRanges(); + } + if ( sel.empty ) { + sel.empty(); + } + } + } + } ); + + $( document ).ready( mw.translate.init ); +} )( jQuery, mediaWiki, autosize ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.recentgroups.js b/www/wiki/extensions/Translate/resources/js/ext.translate.recentgroups.js new file mode 100644 index 00000000..360739d1 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.recentgroups.js @@ -0,0 +1,31 @@ +( function () { + 'use strict'; + + mw.translate = mw.translate || {}; + + /** + * Simple wrapper for storing recent groups for an user. + * + * @class mw.translate.recentGroups + * @singleton + * @since 2016.03 + */ + + mw.translate.recentGroups = { + get: function () { + return JSON.parse( mw.storage.get( 'translate-recentgroups' ) ) || []; + }, + + append: function ( value ) { + var items = this.get(); + + items.unshift( value ); + items = items.filter( function ( item, index, array ) { + return array.indexOf( item ) === index; + } ); + items = items.slice( 0, 5 ); + + mw.storage.set( 'translate-recentgroups', JSON.stringify( items ) ); + } + }; +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.selecttoinput.js b/www/wiki/extensions/Translate/resources/js/ext.translate.selecttoinput.js new file mode 100644 index 00000000..cb4a51bd --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.selecttoinput.js @@ -0,0 +1,27 @@ +window.appendFromSelect = function ( selectid, targetid ) { + 'use strict'; + + var select = document.getElementById( selectid ), + target = document.getElementById( targetid ), + atxt; + + if ( !target || !select ) { + return; + } + + atxt = select.options[ select.selectedIndex ].value; + + if ( !atxt ) { + return; + } + + if ( target.value.replace( /\s+/g, '' ) !== '' ) { + atxt = ', ' + atxt; + } + + atxt = target.value + atxt; + + atxt = atxt.replace( /\bdefault\b[,\s]*/i, '' ); + + target.value = atxt; +}; diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.aggregategroups.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.aggregategroups.js new file mode 100644 index 00000000..ba9ba5f3 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.aggregategroups.js @@ -0,0 +1,364 @@ +( function () { + 'use strict'; + + function getApiParams( $target ) { + return { + action: 'aggregategroups', + aggregategroup: $target.parents( '.mw-tpa-group' ).data( 'groupid' ) + }; + } + + function dissociate( event ) { + var params, + $target = $( event.target ), + api = new mw.Api(); + + function successFunction() { + $target.parent( 'li' ).remove(); + } + + params = $.extend( getApiParams( $target ), { + do: 'dissociate', + group: $target.data( 'groupid' ) + } ); + + api.postWithToken( 'csrf', params ) + .done( successFunction ) + .fail( function ( code, data ) { + // eslint-disable-next-line no-alert + alert( data.error && data.error.info ); + } ); + } + + function associate( event, resp ) { + var successFunction, params, subgroupId, + $target = $( event.target ), + $parent = $target.parents( '.mw-tpa-group' ), + parentId = $parent.data( 'id' ), + subgroupName = $parent.children( '.tp-group-input' ).val(), + api = new mw.Api(); + + successFunction = function () { + var aAttr, $a, spanAttr, $span, $ol; + + aAttr = { + href: mw.util.getUrl( subgroupName ), + title: subgroupName + }; + + $a = $( '<a>', aAttr ).text( subgroupName ); + + spanAttr = { + class: 'tp-aggregate-remove-button', + 'data-groupid': subgroupId + }; + + $span = $( '<span>', spanAttr ); + + $ol = $( '#mw-tpa-grouplist-' + parentId ); + $ol.append( $( '<li>' ).append( $a, $span ) ); + $span.click( dissociate ); + $parent.children( '.tp-group-input' ).val( '' ); + }; + + // Get the label for the value and make API request if valid + subgroupId = ''; + $.each( resp, function ( key, value ) { + if ( subgroupName === value.label ) { + subgroupId = value.id; + } + } ); + + if ( subgroupId ) { + params = $.extend( getApiParams( $target ), { + do: 'associate', + group: subgroupId + } ); + + api.postWithToken( 'csrf', params ) + .done( successFunction ) + .fail( function ( code, data ) { + // eslint-disable-next-line no-alert + alert( data.error && data.error.info ); + } ); + } else { + // eslint-disable-next-line no-alert + alert( mw.msg( 'tpt-invalid-group' ) ); + } + } + + function removeGroup( event ) { + var params, + $target = $( event.target ), + api = new mw.Api(); + + function successFunction() { + $( event.target ).parents( '.mw-tpa-group' ).remove(); + } + + // XXX: 'confirm' is nonstandard. + if ( $.isFunction( window.confirm ) && + // eslint-disable-next-line no-alert + window.confirm( mw.msg( 'tpt-aggregategroup-remove-confirm' ) ) ) { + params = $.extend( getApiParams( $target ), { + do: 'remove' + } ); + + api.postWithToken( 'csrf', params ) + .done( successFunction ) + .fail( function ( code, data ) { + // eslint-disable-next-line no-alert + alert( data.error && data.error.info ); + } ); + } + } + + function editGroup( event ) { + var $target = $( event.target ), + $parent = $target.closest( '.mw-tpa-group' ), + aggregateGroupId = $parent.data( 'groupid' ), + $displayGroup = $parent.children( '.tp-display-group' ), + $editGroup = $parent.children( '.tp-edit-group' ), + successFunction, + params, + aggGroupNameInputName = $editGroup.children( 'input.tp-aggregategroup-edit-name' ), + aggGroupNameInputDesc = $editGroup.children( 'input.tp-aggregategroup-edit-description' ), + aggregateGroupName = aggGroupNameInputName.val(), + aggregateGroupDesc = aggGroupNameInputDesc.val(), + api = new mw.Api(); + + successFunction = function () { + // Replace the text by the new text without altering the other 2 span tags + $displayGroup.children( '.tp-name' ).contents().filter( function () { + return this.nodeType === 3; + } ).replaceWith( aggregateGroupName ); + $displayGroup.children( '.tp-desc' ).text( aggregateGroupDesc ); + $displayGroup.removeClass( 'hidden' ); + $editGroup.addClass( 'hidden' ); + }; + + params = { + action: 'aggregategroups', + do: 'update', + groupname: aggregateGroupName, + groupdescription: aggregateGroupDesc, + aggregategroup: aggregateGroupId + }; + + api.postWithToken( 'csrf', params ) + .done( successFunction ) + .fail( function ( code, data ) { + // eslint-disable-next-line no-alert + alert( data.error.info ); + } ); + } + + function cancelEditGroup( event ) { + var $parent = $( event.target ).closest( '.mw-tpa-group' ); + + $parent.children( '.tp-display-group' ).removeClass( 'hidden' ); + $parent.children( '.tp-edit-group' ).addClass( 'hidden' ); + } + + $( function () { + var excludeFunction, autocompleteFunction, resp, + api = new mw.Api(), + exclude = [], + groups = [], + $input = $( '.tp-group-input' ); + + excludeFunction = function ( event ) { + exclude = []; + + if ( groups.length === 0 ) { + // Get list of subgroups using API + api.get( { + action: 'query', + meta: 'messagegroups', + mgformat: 'tree', + mgroot: 'all', + mgprop: 'label|id' + } ).done( function ( result ) { + groups = result.query.messagegroups; + } ); + } + + // Exclude groups already present + $( event.target ).closest( '.mw-tpa-group' ).find( 'li' ).each( + function ( key, data ) { + // Need to trim to remove the trailing whitespace + // Can't use innerText not supported by Firefox + var groupName = $( data ).text(); + groupName = groupName.trim(); + exclude.push( groupName ); + } + ); + }; + + autocompleteFunction = function ( request, response ) { + // Allow case insensitive search + var inp = new RegExp( request.term, 'i' ); + + resp = []; + + $.each( groups, function ( key, value ) { + if ( value.label.match( inp ) && exclude.indexOf( value.label ) === -1 ) { + resp.push( value ); + } + } ); + response( resp ); + }; + + $input.focus( excludeFunction ); + $input.autocomplete( { + source: autocompleteFunction, + minLength: 0 + } ).focus( function () { + // Enable showing all groups when nothing is entered + $( this ).autocomplete( 'search', $( this ).val() ); + } ); + + $( '.tp-aggregate-add-button' ).click( function ( event ) { + associate( event, resp ); + } ); + $( '.tp-aggregate-remove-button' ).click( dissociate ); + $( '.tp-aggregate-remove-ag-button' ).click( removeGroup ); + $( '.tp-aggregategroup-update' ).click( editGroup ); + $( '.tp-aggregategroup-update-cancel' ).click( cancelEditGroup ); + + $( 'a.tpt-add-new-group' ).on( 'click', function ( event ) { + $( 'div.tpt-add-new-group' ).removeClass( 'hidden' ); + // Link has anchor which goes top of the page + event.preventDefault(); + } ); + + $( '.tp-aggregate-edit-ag-button' ).on( 'click', function ( event ) { + var $parent = $( event.target ).closest( '.mw-tpa-group' ); + + $parent.children( '.tp-display-group' ).addClass( 'hidden' ); + $parent.children( '.tp-edit-group' ).removeClass( 'hidden' ); + } ); + + $( '#tpt-aggregategroups-save' ).on( 'click', function () { + var successFunction, params, + aggGroupNameInputName = $( 'input.tp-aggregategroup-add-name' ), + aggGroupNameInputDesc = $( 'input.tp-aggregategroup-add-description' ), + aggregateGroupName = aggGroupNameInputName.val(), + aggregateGroupDesc = aggGroupNameInputDesc.val(), + api = new mw.Api(); + + // Empty the fields. If they are not emptied, then when another group + // is added, the values will appear again. + aggGroupNameInputName.val( '' ); + aggGroupNameInputDesc.val( '' ); + + successFunction = function ( data ) { + var $removeSpan, $editSpan, $displayHeader, $div, $groupSelector, $addButton, + $cancelButton, $divDisplay, $divEdit, $saveButton, + aggregateGroupId = data.aggregategroups.aggregategroupId; + + $removeSpan = $( '<span>' ).attr( 'id', aggregateGroupId ) + .addClass( 'tp-aggregate-remove-ag-button' ); + $editSpan = $( '<span>' ).attr( 'id', aggregateGroupId ) + .addClass( 'tp-aggregate-edit-ag-button' ); + // Prints the name and the two spans in a single row + $displayHeader = $( '<h2>' ).addClass( 'tp-name' ).text( aggregateGroupName ) + .append( $editSpan, $removeSpan ); + + $divDisplay = $( '<div>' ).addClass( 'tp-display-group' ) + .append( $displayHeader ) + .append( $( '<p>' ).addClass( 'tp-desc' ).text( aggregateGroupDesc ) ); + + $saveButton = $( '<input>' ) + .attr( { + type: 'button', + class: 'tp-aggregategroup-update' + } ) + .val( mw.msg( 'tpt-aggregategroup-update' ) ); + $cancelButton = $( '<input>' ) + .attr( { + type: 'button', + class: 'tp-aggregategroup-update-cancel' + } ) + .val( mw.msg( 'tpt-aggregategroup-update-cancel' ) ); + $divEdit = $( '<div>' ) + .addClass( 'tp-edit-group hidden' ) + .append( $( '<label>' ) + .text( mw.msg( 'tpt-aggregategroup-edit-name' ) ) ) + .append( $( '<input>' ) + .attr( { + class: 'tp-aggregategroup-edit-name', + id: 'tp-agg-name' + } ) + .val( aggregateGroupName ) + ) + .append( $( '<br /><label>' ) + .text( mw.msg( 'tpt-aggregategroup-edit-description' ) ) ) + .append( $( '<input>' ) + .attr( { + class: 'tp-aggregategroup-edit-description', + id: 'tp-agg-desc' + } ) + .val( aggregateGroupDesc ) + ) + .append( $saveButton, $cancelButton ); + + $div = $( '<div>' ).addClass( 'mw-tpa-group' ) + .append( $divDisplay, $divEdit ) + .append( $( '<ol id=\'mw-tpa-grouplist-' + aggregateGroupId + '\'>' ) ); + + $div.data( 'groupid', aggregateGroupId ); + $div.data( 'id', aggregateGroupId ); + + $groupSelector = $( '<input>' ).attr( { + type: 'text', + class: 'tp-group-input' + } ); + $groupSelector.focus( excludeFunction ); + $groupSelector.autocomplete( { + source: autocompleteFunction, + minLength: 0 + } ).focus( function () { + // Enable showing all groups when nothing is entered + $( this ).autocomplete( 'search', $( this ).val() ); + } ); + $addButton = $( '<input>' ) + .attr( { + type: 'button', + class: 'tp-aggregate-add-button', + id: aggregateGroupId + } ) + .val( mw.msg( 'tpt-aggregategroup-add' ) ); + $div.append( $groupSelector, $addButton ); + $addButton.click( function ( event ) { + associate( event, resp ); + } ); + $editSpan.on( 'click', function ( event ) { + var $parent = $( event.target ).closest( '.mw-tpa-group' ); + $parent.children( '.tp-display-group' ).addClass( 'hidden' ); + $parent.children( '.tp-edit-group' ).removeClass( 'hidden' ); + } ); + + $saveButton.click( editGroup ); + $cancelButton.click( cancelEditGroup ); + $removeSpan.click( removeGroup ); + $( 'div.tpt-add-new-group' ).addClass( 'hidden' ); + $( 'a.tpt-add-new-group' ).before( $div ); + }; + + params = { + action: 'aggregategroups', + do: 'add', + groupname: aggregateGroupName, + groupdescription: aggregateGroupDesc + }; + + api.postWithToken( 'csrf', params ) + .done( successFunction ) + .fail( function ( code, data ) { + // eslint-disable-next-line no-alert + alert( data.error && data.error.info ); + } ); + } ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.importtranslations.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.importtranslations.js new file mode 100644 index 00000000..1ba24709 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.importtranslations.js @@ -0,0 +1,20 @@ +( function () { + 'use strict'; + + function buttoner( $input ) { + if ( $input.val ) { + $( 'input[type=submit]' ).prop( 'disabled', false ); + } else { + $( 'input[type=submit]' ).prop( 'disabled', true ); + } + } + + $( function () { + var $input = $( '#mw-translate-up-local-input' ); + $input.on( 'change', function () { + buttoner( $input ); + } ); + + buttoner( $input ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.languagestats.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.languagestats.js new file mode 100644 index 00000000..979242d8 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.languagestats.js @@ -0,0 +1,136 @@ +/*! + * Collapsing script for Special:LanguageStats in MediaWiki Extension:Translate + * @author Krinkle <krinklemail (at) gmail (dot) com> + * @author Niklas Laxström + * @license GPL-2.0-or-later, CC-BY-SA-3.0 + */ + +( function () { + 'use strict'; + + /** + * Add css class to every other visible row. + * It's not possible to do zebra colors with CSS only if there are hidden rows. + */ + function doZebra() { + $( '.statstable tr:visible:odd' ).toggleClass( 'tux-statstable-even', false ); + $( '.statstable tr:visible:even' ).toggleClass( 'tux-statstable-even', true ); + } + + $( function () { + var $allChildRows, $allTogglesCache, $toggleAllButton, + $translateTable = $( '.statstable' ), + $metaRows = $( 'tr.AggregateMessageGroup', $translateTable ); + + // Quick return + if ( !$metaRows.length ) { + return; + } + + $metaRows.each( function () { + var $toggler, + $parent = $( this ), + thisGroupId = $parent.attr( 'data-groupid' ), + $children = $( 'tr[data-parentgroup="' + thisGroupId + '"]', $translateTable ); + + // Only do the collapse stuff if this Meta-group actually has children on this page + if ( !$children.length ) { + return; + } + + // Build toggle link + $toggler = $( '<span class="groupexpander collapsed">[</span>' ) + .append( $( '<a href="#"></a>' ) + .text( mw.msg( 'translate-langstats-expand' ) ) ) + .append( ']' ) + .click( function ( e ) { + var $el = $( this ); + // Switch the state and toggle the rows + if ( $el.hasClass( 'collapsed' ) ) { + $children.fadeIn( { start: doZebra } ).trigger( 'show' ); + $el.removeClass( 'collapsed' ).addClass( 'expanded' ); + $el.find( '> a' ).text( mw.msg( 'translate-langstats-collapse' ) ); + } else { + $children.fadeOut( { done: doZebra } ).trigger( 'hide' ); + $el.addClass( 'collapsed' ).removeClass( 'expanded' ); + $el.find( '> a' ).text( mw.msg( 'translate-langstats-expand' ) ); + } + + e.preventDefault(); + } ); + + // Add the toggle link to the first cell of the meta group table-row + $parent.find( ' > td:first' ).append( $toggler ); + + // Handle hide/show recursively, so that collapsing parent group + // hides all sub groups regardless of nesting level + $parent.on( 'hide show', function ( event ) { + // Reuse $toggle, $parent and $children from parent scope + if ( $toggler.hasClass( 'expanded' ) ) { + $children.trigger( event.type )[ event.type ](); + } + } ); + } ); + + // Create, bind and append the toggle-all button + $allChildRows = $( 'tr[data-parentgroup]', $translateTable ); + $allTogglesCache = null; + $toggleAllButton = $( '<span class="collapsed">[</span>' ) + .append( $( '<a href="#"></a>' ) + .text( mw.msg( 'translate-langstats-expandall' ) ) ) + .append( ']' ) + .click( function ( e ) { + var $el = $( this ), + $allToggles = $allTogglesCache || $( '.groupexpander', $translateTable ); + + // Switch the state and toggle the rows + // and update the local toggles too + if ( $el.hasClass( 'collapsed' ) ) { + $allChildRows.show(); + $el.add( $allToggles ).removeClass( 'collapsed' ).addClass( 'expanded' ); + $el.find( '> a' ).text( mw.msg( 'translate-langstats-collapseall' ) ); + $allToggles.find( '> a' ).text( mw.msg( 'translate-langstats-collapse' ) ); + } else { + $allChildRows.hide(); + $el.add( $allToggles ).addClass( 'collapsed' ).removeClass( 'expanded' ); + $el.find( '> a' ).text( mw.msg( 'translate-langstats-expandall' ) ); + $allToggles.find( '> a' ).text( mw.msg( 'translate-langstats-expand' ) ); + } + + doZebra(); + e.preventDefault(); + } ); + + // Initially hide them + $allChildRows.hide(); + doZebra(); + + // Add the toggle-all button above the table + $( '<p class="groupexpander-all"></p>' ).append( $toggleAllButton ).insertBefore( $translateTable ); + } ); + + $( function () { + var index, + sort = {}, + re = /#sortable:(\d+)=(asc|desc)/, + match = re.exec( window.location.hash ), + $tables = $( '.statstable' ); + + if ( match ) { + index = parseInt( match[ 1 ], 10 ); + sort[ index ] = match[ 2 ]; + } + $tables.tablesorter( { sortList: [ sort ] } ); + + $tables.on( 'sortEnd.tablesorter', function () { + var $table = $( this ); + $table.find( '.headerSortDown, .headerSortUp' ).each( function () { + var index = $table.find( 'th' ).index( $( this ) ), + dir = $( this ).hasClass( 'headerSortUp' ) ? 'asc' : 'desc'; + window.location.hash = 'sortable:' + index + '=' + dir; + + doZebra(); + } ); + } ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.managetranslatorsandbox.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.managetranslatorsandbox.js new file mode 100644 index 00000000..e574e9f2 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.managetranslatorsandbox.js @@ -0,0 +1,755 @@ +/*! + * JS for special page. + * @author Niklas Laxström + * @author Sucheta Ghoshal + * @author Amir E. Aharoni + * @author Pau Giner + * @license GPL-2.0-or-later + */ + +( function () { + 'use strict'; + + var delay; + + /** + * A callback for sorting translations. + * + * @param {Object} translationA Object loaded from translation stash + * @param {Object} translationB Object loaded from translation stash + * @return {number} String comparison of language codes + */ + function sortTranslationsByLanguage( translationA, translationB ) { + var a = translationA.title.split( '/' ).pop(), + b = translationB.title.split( '/' ).pop(); + + return a.localeCompare( b ); + } + + function doApiAction( options ) { + var api = new mw.Api(); + + options = $.extend( {}, { action: 'translatesandbox' }, options ); + + return api.postWithToken( 'csrf', options ).promise(); + } + + function removeSelectedRequests() { + var $nextRequest, + $selectedRequests = $( '.request-selector:checked' ); + + $nextRequest = $selectedRequests + .first() // First selected request + .closest( '.request' ) // The request corresponds that checkbox + .prevAll( ':not(.hide)' ) // Go back till a non-hidden request + .first(); // The above selecter gives list from bottom to top. Select the bottom one. + + $selectedRequests.closest( '.request' ).remove(); + + updateRequestCount(); + + if ( !$nextRequest.length ) { + // If there's no request above the first checked request, + // try to get the first request in the column + $nextRequest = $( '.requests .request:not(.hide)' ).first(); + } + + if ( $nextRequest.length ) { + $nextRequest.click(); + updateSelectedIndicator( 1 ); + } else { + updateSelectedIndicator( 0 ); + } + } + + /** + * Display the request details when user clicks on a request item + * + * @param {Object} request The request data set from backend on request items + */ + function displayRequestDetails( request ) { + var storage, + $reminderStatus = $( '<span>' ).addClass( 'reminder-status' ), + $detailsPane = $( '.details.pane' ); + + if ( request.reminderscount ) { + $reminderStatus.text( mw.msg( + 'tsb-reminder-sent', + request.reminderscount, + request.lastreminder + ) ); + } + + $detailsPane.empty().append( + $( '<div>' ) + .addClass( 'tsb-header row' ) + .text( request.username ), + $( '<div>' ) + .addClass( 'reminder-email row' ) + .append( + $( '<span>' ).text( request.email ), + $( '<a>' ) + .prop( 'href', '#' ) + .addClass( 'send-reminder link' ) + .text( mw.msg( 'tsb-reminder-link-text' ) ) + .on( 'click', function ( e ) { + e.preventDefault(); + + $reminderStatus + .text( mw.msg( 'tsb-reminder-sending' ) ); + + doApiAction( { + do: 'remind', + userid: request.userid + } ).done( function () { + $reminderStatus.text( mw.msg( 'tsb-reminder-sent-new' ) ); + } ).fail( function () { + $reminderStatus.text( mw.msg( 'tsb-reminder-failed' ) ); + } ); + } ), + $reminderStatus + ), + $( '<div>' ) + .addClass( 'languages row autonym' ), + $( '<div>' ) + .addClass( 'signup-comment row' ), + $( '<div>' ) + .addClass( 'actions row' ) + .append( + $( '<button>' ) + .addClass( 'accept mw-ui-button mw-ui-progressive' ) + .text( mw.msg( 'tsb-accept-button-label' ) ) + .on( 'click', function () { + mw.notify( mw.msg( 'tsb-accept-confirmation', 1 ) ); + + window.tsbUpdatingUsers = true; + + doApiAction( { + userid: request.userid, + do: 'promote' + } ).done( function () { + removeSelectedRequests(); + + window.tsbUpdatingUsers = false; + } ); + } ), + $( '<button>' ) + .addClass( 'reject mw-ui-button mw-ui-destructive' ) + .text( mw.msg( 'tsb-reject-button-label' ) ) + .on( 'click', function () { + mw.notify( mw.msg( 'tsb-reject-confirmation', 1 ) ); + + window.tsbUpdatingUsers = true; + + doApiAction( { + userid: request.userid, + do: 'delete' + } ).done( function () { + removeSelectedRequests(); + + window.tsbUpdatingUsers = false; + } ); + } ) + ), + $( '<div>' ) + .addClass( 'translations row' ) + ); + + if ( request.languagepreferences ) { + if ( request.languagepreferences.languages ) { + $.each( request.languagepreferences.languages, function ( index, language ) { + $detailsPane.find( '.languages' ).append( + $( '<span>' ) + .prop( { + dir: $.uls.data.getDir( language ), + lang: language + } ) + .text( $.uls.data.getAutonym( language ) ) + ); + } ); + } + + if ( request.languagepreferences.comment ) { + $detailsPane.find( '.signup-comment' ).append( + $( '<div>' ) + .addClass( 'signup-comment-label' ) + .text( mw.msg( 'tsb-user-posted-a-comment' ) ), + $( '<div>' ) + .addClass( 'signup-comment-text' ) + .text( request.languagepreferences.comment ) + ); + } + } + + // @todo: move higher in the tree + storage = new mw.translate.TranslationStashStorage(); + storage.getUserTranslations( request.username ).done( showTranslations ); + } + + function showTranslations( translations ) { + var gender, + $target = $( '.translations' ); + + $target.empty(); + + // Display a message if the user didn't make any translations + if ( !translations.translationstash.translations.length ) { + $target.append( + $( '<div>' ) + .addClass( 'tsb-details-no-translations' ) + .text( mw.msg( 'tsb-didnt-make-any-translations' ) ) + ); + + return; + } + + gender = $( '.requests-list .request.selected' ).data( 'data' ).gender; + $target.append( + $( '<div>' ) + .addClass( 'row title' ) + .append( + $( '<div>' ) + .text( mw.msg( 'tsb-translations-source' ) ) + .addClass( 'four columns' ), + $( '<div>' ) + .text( mw.msg( 'tsb-translations-user', gender ) ) + .addClass( 'four columns' ), + $( '<div>' ) + .text( mw.msg( 'tsb-translations-current' ) ) + .addClass( 'four columns' ) + ) + ); + + translations.translationstash.translations.sort( sortTranslationsByLanguage ); + $.each( translations.translationstash.translations, function ( index, translation ) { + showTranslation( translation ); + } ); + } + + function showTranslation( translation ) { + var $target = $( '.translations' ), + translationLang = translation.title.split( '/' ).pop(); + + $target.append( $( '<div>' ) + .addClass( 'row' ) + .append( + $( '<div>' ) + .addClass( 'four columns source' ) + .text( translation.definition ), + $( '<div>' ) + .addClass( 'four columns translation' ) + .append( + $( '<div>' ).text( translation.translation ), + $( '<div>' ) + .addClass( 'info autonym' ) + .prop( { + dir: $.uls.data.getDir( translationLang ), + lang: translationLang + } ) + .text( + $.uls.data.getAutonym( translationLang ) + ) + ), + $( '<div>' ) + .addClass( 'four columns comparison' ) + .append( + $( '<div>' ).text( translation.comparison || '' ), + $( '<div>' ) + .addClass( 'info' ) + .text( translation.title ) + ) + ) + ); + } + + /** + * Display when multiple requests are checked. + */ + function displayOnMultipleSelection() { + var selectedUserIDs = $( '.request-selector:checked' ).map( function ( i, checkedBox ) { + return $( checkedBox ).parents( 'div.request' ).data( 'data' ).userid; + } ); + + selectedUserIDs = selectedUserIDs.toArray(); + + $( '.details.pane' ).empty().append( + $( '<div>' ) + .addClass( 'tsb-header row' ), + $( '<div>' ) + .addClass( 'actions row' ) + .append( + $( '<button>' ) + .addClass( 'accept-all mw-ui-button mw-ui-progressive' ) + .text( mw.msg( 'tsb-accept-all-button-label' ) ) + .on( 'click', function () { + mw.notify( mw.msg( 'tsb-accept-confirmation', selectedUserIDs.length ) ); + + window.tsbUpdatingUsers = true; + + doApiAction( { + userid: selectedUserIDs, + do: 'promote' + } ).done( function () { + removeSelectedRequests(); + + window.tsbUpdatingUsers = false; + } ); + } ), + $( '<button>' ) + .addClass( 'reject-all mw-ui-button mw-ui-destructive' ) + .text( mw.msg( 'tsb-reject-all-button-label' ) ) + .on( 'click', function () { + mw.notify( mw.msg( 'tsb-reject-confirmation', selectedUserIDs.length ) ); + + window.tsbUpdatingUsers = true; + + doApiAction( { + userid: selectedUserIDs, + do: 'delete' + } ).done( function () { + removeSelectedRequests(); + + window.tsbUpdatingUsers = false; + } ); + } ) + ) + ); + } + + /** + * Updates the counter of the selected users. + * + * @param {number} count The number of selected users + */ + function updateSelectedIndicator( count ) { + var text = mw.msg( 'tsb-selected-count', mw.language.convertNumber( count ) ); + + $( '.requests.pane .request-footer .selected-counter' ).text( text ); + if ( count > 1 ) { + $( '.details.pane .tsb-header' ).text( text ); + } + } + + /** + * Returns older requests with the same number of translations. + * + * @return {jQuery} Older requests + */ + function getOlderRequests() { + var $lastSelectedRequest = $( '.row.request.selected' ).last(), + currentTranslationCount = $lastSelectedRequest.data( 'data' ).translations; + + return $lastSelectedRequest.nextAll( ':not(.hide)' ).filter( function () { + return ( $( this ).data( 'data' ).translations === currentTranslationCount ); + } ); + } + + /** + * Updates the number of older requests with the same number + * of translations at the link in the bottom of the requests row + * or hides that link if there are no such requests. + */ + function indicateOlderRequests() { + var oldRequestsCount, oldRequestsCountString, + $olderRequests = getOlderRequests(), + $olderRequestsIndicator = $( '.older-requests-indicator' ); + + oldRequestsCount = $olderRequests.length; + oldRequestsCountString = mw.language.convertNumber( oldRequestsCount ); + + if ( oldRequestsCount ) { + $olderRequestsIndicator + .text( mw.msg( 'tsb-older-requests', oldRequestsCountString ) ) + .removeClass( 'hide' ); + } else { + $olderRequestsIndicator + .addClass( 'hide' ); + } + } + + /** + * Updates the number of requests. + */ + function updateRequestCount() { + var $requests = $( '.requests-list .request' ), + visibleRequestsCount = $requests.filter( ':not(.hide)' ).length; + + $( '.request-count' ).text( + mw.msg( 'tsb-request-count', mw.language.convertNumber( visibleRequestsCount ) ) + ); + + if ( $requests.length === 0 ) { + $( '.details.pane' ) + .empty() + .append( + $( '<div>' ) + .addClass( 'tsb-header row' ) + .text( mw.msg( 'tsb-no-requests-from-new-users' ) ) + ); + } + } + + /** + * Sets the height of the panes to the window height. + */ + function setPanesHeight() { + var $detailsPane = $( '.details.pane' ), + $requestsPane = $( '.requests.pane' ), + detailsHeight = $( window ).height() - $detailsPane.offset().top, + requestsHeight = detailsHeight - + $requestsPane.find( '.request-footer' ).height() - + $requestsPane.find( '.request-header' ).height(); + + $detailsPane.css( 'max-height', detailsHeight ); + $requestsPane.find( '.requests-list' ).css( 'max-height', requestsHeight ); + } + + function selectAllRequests() { + var selectedCount, + $requestCheckboxes = $( '.request-selector' ), + $detailsPane = $( '.details.pane' ), + $selectAll = $( '.request-selector-all' ), + $requestRows = $( '.requests .request' ), + selectAllChecked = $selectAll.prop( 'checked' ), + $visibleRows = $requestRows.not( '.hide' ); + + $visibleRows.each( function ( index, row ) { + $( row ).find( '.request-selector' ).prop( { + checked: selectAllChecked, + disabled: false + } ); + } ); + + if ( selectAllChecked ) { + displayOnMultipleSelection(); + $visibleRows.addClass( 'selected' ); + selectedCount = $requestCheckboxes.filter( ':checked' ).length; + } else { + $detailsPane.empty(); + $requestRows.removeClass( 'selected' ); + selectedCount = 0; + } + + updateSelectedIndicator( selectedCount ); + indicateOlderRequests(); + } + + /** + * Handle click on request row + * + * @param {jQuery.Event} e + */ + function onSelectRequest( e ) { + var $requestRow = $( e.target ).closest( '.request' ), + $requestRows = $( '.requests .request' ), + $selectAll = $( '.request-selector-all' ); + + displayRequestDetails( $requestRow.data( 'data' ) ); + + // Clicking a row makes only that row selected and unselects all other rows + $requestRows.each( function ( i, row ) { + var $row = $( row ); + + if ( row === $requestRow[ 0 ] ) { + $row.addClass( 'selected' ) + .find( '.request-selector' ).prop( { + checked: true, + disabled: true + } ); + } else { + $row.removeClass( 'selected' ) + .find( '.request-selector' ).prop( { + checked: false, + disabled: false + } ); + } + } ); + + $selectAll.prop( 'indeterminate', true ); + + updateSelectedIndicator( 1 ); + indicateOlderRequests(); + } + + /** + * Event handler for request checkbox selection. + * + * @param {jQuery.Event} e + */ + function requestSelectHandler( e ) { + var checkedCount, $checkedBoxes, + request = e.target, + $detailsPane = $( '.details.pane' ), + $requestCheckboxes = $( '.request-selector' ), + $selectAll = $( '.request-selector-all' ), + $thisRequestRow = $( request ).parents( 'div.request' ); + + // Uncheck the rows that were selected by clicking the row + $requestCheckboxes.filter( ':disabled' ).prop( 'disabled', false ); + + if ( request.checked ) { + $thisRequestRow.addClass( 'selected' ); + } else { + $thisRequestRow.removeClass( 'selected' ); + } + + $checkedBoxes = $requestCheckboxes.filter( ':checked' ); + checkedCount = $checkedBoxes.length; + + if ( checkedCount === $requestCheckboxes.length ) { + // All boxes are selected + $selectAll.prop( { + checked: true, + indeterminate: false + } ); + + displayOnMultipleSelection(); + } else if ( checkedCount === 0 ) { + // No boxes are selected + $selectAll.prop( { + checked: false, + indeterminate: false + } ); + + $detailsPane.empty(); + } else if ( checkedCount === 1 ) { + $selectAll.prop( { + checked: false, + indeterminate: true + } ); + + $checkedBoxes.prop( 'disabled', true ); + + // Here we know that only one checkbox is selected, + // so it's OK to query the data from it + displayRequestDetails( $checkedBoxes.parents( 'div.request' ).data( 'data' ) ); + } else { + $selectAll.prop( { + checked: false, + indeterminate: true + } ); + + displayOnMultipleSelection(); + } + + updateSelectedIndicator( checkedCount ); + indicateOlderRequests(); + + e.stopPropagation(); + } + + /** + * Old request click handler. + * + * @param {jQuery.Event} e + */ + function oldRequestSelector( e ) { + e.preventDefault(); + + getOlderRequests().each( function ( index, request ) { + $( request ).find( '.request-selector' ) + .prop( 'checked', true ) // Otherwise the state doesn't actually change + .change(); + } ); + } + + // ====================================== + // LanguageFilter plugin + // ====================================== + function LanguageFilter( element ) { + this.$selector = $( element ); + this.init(); + } + + LanguageFilter.prototype.init = function () { + var languageFilter = this, + $clearButton; + + $clearButton = $( '<button>' ) + .addClass( 'clear-language-selector hide' ) + .text( '×' ); + + languageFilter.$selector.after( $clearButton ); + // Activate language selector + languageFilter.$selector.uls( { + onSelect: function ( language ) { + languageFilter.$selector + .removeClass( 'unselected' ) + .addClass( 'selected autonym' ) + .prop( { + dir: $.uls.data.getDir( language ), + lang: language + } ) + .text( $.uls.data.getAutonym( language ) ); + + languageFilter.filter( language ); + $clearButton.removeClass( 'hide' ); + indicateOlderRequests(); + }, + ulsPurpose: 'translate-special-managetranslatorsandbox', + quickList: mw.uls.getFrequentLanguageList + } ); + + $clearButton.on( 'click', function () { + var userLang = mw.config.get( 'wgUserLanguage' ); + + languageFilter.$selector + .removeClass( 'selected autonym' ) + .prop( { + dir: $.uls.data.getDir( userLang ), + lang: userLang + } ) + .addClass( 'unselected' ) + .text( mw.msg( 'tsb-all-languages-button-label' ) ); + + languageFilter.filter(); + $clearButton.addClass( 'hide' ); + } ); + }; + + /** + * Filter the requests by language. + * + * @param {string} [language] Language code + */ + LanguageFilter.prototype.filter = function ( language ) { + var $requests = $( '.request' ); + + $requests.each( function ( index, request ) { + var $request = $( request ), + requestData = $request.data( 'data' ); + + if ( !language || + ( requestData.languagepreferences && + requestData.languagepreferences.languages && + requestData.languagepreferences.languages.indexOf( language ) > -1 ) + ) { + // Found language + $request.removeClass( 'hide' ); + } else { + $request.addClass( 'hide' ); + } + } ); + + updateAfterFiltering(); + }; + + $.fn.languageFilter = function () { + return this.each( function () { + if ( !$.data( this, 'LanguageFilter' ) ) { + $.data( this, 'LanguageFilter', new LanguageFilter( this ) ); + } + } ); + }; + + // ====================================== + // TranslatorSearch plugin + // ====================================== + function TranslatorSearch( element ) { + this.$search = $( element ); + this.init(); + } + + TranslatorSearch.prototype.init = function () { + this.$search.on( 'search keyup', this.keyup.bind( this ) ); + }; + + TranslatorSearch.prototype.keyup = function () { + var query, + translatorSearch = this; + + // Respond to the keypress events after a small timeout to avoid freeze when typed fast + delay( function () { + query = translatorSearch.$search.val().trim().toLowerCase(); + translatorSearch.filter( query ); + }, 300 ); + }; + + TranslatorSearch.prototype.filter = function ( query ) { + var $requests = $( '.request' ); + + $requests.each( function ( index, request ) { + var $request = $( request ), + requestData = $request.data( 'data' ); + + if ( query.length === 0 || + requestData.username.toLowerCase().indexOf( query ) === 0 || + requestData.email.toLowerCase().indexOf( query ) === 0 + ) { + $request.removeClass( 'hide' ); + } else { + $request.addClass( 'hide' ); + } + } ); + + updateAfterFiltering(); + }; + + function updateAfterFiltering() { + var $selectedRequests, + $firstVisibleUser = $( '.request:not(.hide)' ).first(); + + if ( $firstVisibleUser.length ) { + $firstVisibleUser.click(); + } else { + $( '.details.pane' ).empty(); + $selectedRequests = $( '.request-selector:checked' ); + $selectedRequests.closest( '.request' ).removeClass( 'selected' ); + $selectedRequests.prop( { + checked: false, + disabled: false + } ); + + updateSelectedIndicator( 0 ); + } + + updateRequestCount(); + } + + $.fn.translatorSearch = function () { + return this.each( function () { + if ( !$.data( this, 'TranslatorSearch' ) ) { + $.data( this, 'TranslatorSearch', new TranslatorSearch( this ) ); + } + } ); + }; + + delay = ( function () { + var timer = 0; + + return function ( callback, milliseconds ) { + clearTimeout( timer ); + timer = setTimeout( callback, milliseconds ); + }; + }() ); + + $( function () { + var $requestCheckboxes = $( '.request-selector' ), + $selectAll = $( '.request-selector-all' ), + $requestRows = $( '.requests .request' ); + + // Delay so we get the correct height on page load + window.setTimeout( setPanesHeight, 0 ); + $( window ).on( 'resize', setPanesHeight ); + + $( '.request-filter-box' ).translatorSearch(); + $( '.language-selector' ).languageFilter(); + + // Handle clicks for the 'Select all' checkbox + $selectAll.on( 'click', selectAllRequests ); + + // Handle clicks on request checkboxes. + $requestCheckboxes.on( 'click change', requestSelectHandler ); + + // Handle clicks on request rows. + $requestRows.on( 'click', onSelectRequest ); + + $( '.older-requests-indicator' ).on( 'click', oldRequestSelector ); + + if ( $requestRows.length ) { + $requestRows.first().click(); + } + + updateRequestCount(); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.operatorsuggest.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.operatorsuggest.js new file mode 100644 index 00000000..1115824f --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.operatorsuggest.js @@ -0,0 +1,39 @@ +/* + * Autocomplete search operators. + */ +( function () { + 'use strict'; + + function autocompleteOperators( request, response ) { + var operators = [ 'language:', 'group:', 'filter:' ], + result = [], + lastterm = request.term.split( ' ' ).pop(); + + $.each( operators, function ( index, value ) { + var pos = value.indexOf( lastterm ); + if ( pos === 0 ) { + result.push( value ); + } + } ); + response( result ); + } + + $( '.tux-searchpage .searchinputbox' ) + .autocomplete( { + source: autocompleteOperators, + select: function ( event, ui ) { + var $value = $( this ).val(), + operators = $value.split( ' ' ); + + operators.pop(); + operators.push( ui.item.value ); + + $( this ).val( operators.join( ' ' ) ); + return false; + }, + + focus: function ( event ) { + event.preventDefault(); + } + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js new file mode 100644 index 00000000..3713ac56 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js @@ -0,0 +1,523 @@ +( function () { + 'use strict'; + var noOfSourceUnits, noOfTranslationUnits, + pageName = '', + langCode = '', + sourceUnits = []; + + /** + * Create translation pages using content of right hand side blocks + * and identifiers from left hand side blocks. Create pages only if + * content is not empty. + * + * @param {number} i Array index to sourceUnits. + * @param {string} content + * @return {Function} Returns a function which returns a jQuery.Promise + */ + function createTranslationPage( i, content ) { + + return function () { + var identifier, title, summary, + api = new mw.Api(); + + identifier = sourceUnits[ i ].identifier; + title = 'Translations:' + pageName + '/' + identifier + '/' + langCode; + summary = $( '#pm-summary' ).val(); + + return api.postWithToken( 'csrf', { + action: 'edit', + watchlist: 'nochange', + title: title, + text: content, + summary: summary + } ); + }; + } + + /** + * Get the old translations of a given page at given time. + * + * @param {string} fuzzyTimestamp Timestamp in MediaWiki format + * @param {string} pageTitle + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {Array} return.done.data Array of old translations + */ + function splitTranslationPage( fuzzyTimestamp, pageTitle ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + prop: 'revisions', + rvprop: 'content', + rvstart: fuzzyTimestamp, + titles: pageTitle + } ).then( function ( data ) { + var pageContent, oldTranslationUnits, obj, page, + errorBox = $( '.mw-tpm-sp-error__message' ); + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + if ( typeof obj === undefined ) { + // obj was not initialized + errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + if ( obj.revisions === undefined ) { + // the case of /en subpage where first edit is by FuzzyBot + errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + pageContent = obj.revisions[ 0 ][ '*' ]; + oldTranslationUnits = pageContent.split( '\n\n' ); + return oldTranslationUnits; + } ); + } + + /** + * Get the timestamp before FuzzyBot's first edit on page. + * + * @param {string} pageTitle + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.data + */ + function getFuzzyTimestamp( pageTitle ) { + var api = new mw.Api(); + + // This api call returns the timestamp of FuzzyBot's edit + return api.get( { + action: 'query', + prop: 'revisions', + rvprop: 'timestamp', + rvuser: 'FuzzyBot', + rvdir: 'newer', + titles: pageTitle + } ).then( function ( data ) { + var timestampFB, dateFB, timestampOld, + page, obj, + errorBox = $( '.mw-tpm-sp-error__message' ); + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + // Page does not exist if missing field is present + if ( obj.missing === '' ) { + errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + // Page exists, but no edit by FuzzyBot + if ( obj.revisions === undefined ) { + errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } else { + // FB over here refers to FuzzyBot + timestampFB = obj.revisions[ 0 ].timestamp; + dateFB = new Date( timestampFB ); + dateFB.setSeconds( dateFB.getSeconds() - 1 ); + timestampOld = dateFB.toISOString(); + mw.log( 'New Timestamp: ' + timestampOld ); + return timestampOld; + } + } ); + } + + /** + * Get the translation units created by Translate extension. + * + * @param {string} pageName + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {Object[]} return.done.data Array of sUnit Objects + */ + function getSourceUnits( pageName ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + list: 'messagecollection', + mcgroup: 'page-' + pageName, + mclanguage: 'en', + mcprop: 'definition' + } ).then( function ( data ) { + var result, i, sUnit, key; + sourceUnits = []; + result = data.query.messagecollection; + for ( i = 1; i < result.length; i++ ) { + sUnit = {}; + key = result[ i ].key; + sUnit.identifier = key.slice( key.lastIndexOf( '/' ) + 1 ); + sUnit.definition = result[ i ].definition; + sourceUnits.push( sUnit ); + } + return sourceUnits; + } ); + } + + /** + * Shift rows up by one unit. This is called after a unit is deleted. + * + * @param {jQuery} $start The starting node + */ + function shiftRowsUp( $start ) { + var nextVal, + $current = $start, + $next = $start.next(); + + while ( $next.length ) { + nextVal = $next.find( '.mw-tpm-sp-unit__target' ).val(); + $current.find( '.mw-tpm-sp-unit__target' ).val( nextVal ); + $current = $next; + $next = $current.next(); + } + if ( $current.find( '.mw-tpm-sp-unit__source' ).val() ) { + $current.find( '.mw-tpm-sp-unit__target' ).val( '' ); + } else { + $current.remove(); + } + } + + /** + * Shift rows down by one unit. This is called after a new empty unit is + * added. + * + * @param {jQuery} $nextRow The next row to start with + * @param {string} text The text of the next row + * @return {string} text The text of the last row + */ + function shiftRowsDown( $nextRow, text ) { + var oldText; + + while ( $nextRow.length ) { + oldText = $nextRow.find( '.mw-tpm-sp-unit__target' ).val(); + $nextRow.find( '.mw-tpm-sp-unit__target' ).val( text ); + $nextRow = $nextRow.next(); + text = oldText; + } + return text; + } + + /** + * Create a new row of source text and target text with action icons. + * + * @param {string} sourceText + * @param {string} targetText + * @return {jQuery} newUnit The new row unit object + */ + + function createNewUnit( sourceText, targetText ) { + var newUnit, sourceUnit, targetUnit, actionUnit; + + newUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit row' ); + sourceUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__source five columns' ) + .prop( 'readonly', true ).attr( 'tabindex', '-1' ).val( sourceText ); + targetUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__target five columns' ) + .val( targetText ).prop( 'dir', $.uls.data.getDir( langCode ) ); + actionUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit__actions two columns' ); + actionUnit.append( + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--add' ) + .attr( 'title', mw.msg( 'pm-add-icon-hover-text' ) ), + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--swap' ) + .attr( 'title', mw.msg( 'pm-swap-icon-hover-text' ) ), + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--delete' ) + .attr( 'title', mw.msg( 'pm-delete-icon-hover-text' ) ) + ); + newUnit.append( sourceUnit, targetUnit, actionUnit ); + return newUnit; + } + + /** + * Display the source and target units alongwith the action icons. + * + * @param {Array} sourceUnits + * @param {Array} translations + */ + function displayUnits( sourceUnits, translations ) { + var i, totalUnits, newUnit, unitListing, + sourceText, targetText; + + noOfSourceUnits = sourceUnits.length; + noOfTranslationUnits = translations.length; + totalUnits = noOfSourceUnits > noOfTranslationUnits ? noOfSourceUnits : noOfTranslationUnits; + unitListing = $( '.mw-tpm-sp-unit-listing' ); + unitListing.html( '' ); + for ( i = 0; i < totalUnits; i++ ) { + sourceText = targetText = ''; + if ( sourceUnits[ i ] !== undefined ) { + sourceText = sourceUnits[ i ].definition; + } + if ( translations[ i ] !== undefined ) { + targetText = translations[ i ]; + } + newUnit = createNewUnit( sourceText, targetText ); + unitListing.append( newUnit ); + } + } + + /** + * Split headers from remaining text in each translation unit if present. + * + * @param {Array} translations Array of initial units obtained on splitting + * @return {string[]} Array having the headers split into new unit + */ + function splitHeaders( translations ) { + return $.map( translations, function ( elem ) { + // Check https://regex101.com/r/oT7fZ2 for details + return elem.match( /(^==.+$|(?:(?!^==).+\n?)+)/gm ); + } ); + } + + /** + * Get the index of next translation unit containing h2 header. + * + * @param {number} startIndex Index to start the scan from + * @param {string[]} translationUnits Segmented units. + * @return {number} Index of the next unit found, -1 if not. + */ + function getHeaderUnit( startIndex, translationUnits ) { + var i, regex; + regex = new RegExp( /^==[^=]+==$/m ); + for ( i = startIndex; i < translationUnits.length; i++ ) { + if ( regex.test( translationUnits[ i ] ) ) { + return i; + } + } + return -1; + } + + /** + * Align h2 headers in the order they appear. + * Assumption: The source headers and translation headers appear in + * the same order. + * + * @param {Object[]} sourceUnits + * @param {string[]} translationUnits + * @return {string[]} + */ + function alignHeaders( sourceUnits, translationUnits ) { + var i, regex, tIndex = 0, + matchText, emptyCount, mergeText; + + regex = new RegExp( /^==[^=]+==$/m ); + for ( i = 0; i < sourceUnits.length; i++ ) { + if ( regex.test( sourceUnits[ i ].definition ) ) { + tIndex = getHeaderUnit( tIndex, translationUnits ); + mergeText = ''; + // search is over + if ( tIndex === -1 ) { + break; + } + // remove the unit + matchText = translationUnits.splice( tIndex, 1 ).toString(); + emptyCount = i - tIndex; + if ( emptyCount > 0 ) { + // add empty units + while ( emptyCount !== 0 ) { + translationUnits.splice( tIndex, 0, '' ); + emptyCount -= 1; + } + } else if ( emptyCount < 0 ) { + // merge units until there is room for tIndex translation unit to + // align with ith source unit + while ( emptyCount !== 0 ) { + mergeText += translationUnits.splice( i, 1 ).toString() + '\n'; + emptyCount += 1; + } + if ( i !== 0 ) { + translationUnits[ i - 1 ] += '\n' + mergeText; + } else { + matchText = mergeText + matchText; + } + } + // add the unit back + translationUnits.splice( i, 0, matchText ); + tIndex = i + 1; + } + } + return translationUnits; + } + + /** + * Handler for 'Save' button click event. + */ + function saveHandler() { + var i, content, list = []; + + $( '.mw-tpm-sp-error__message' ).hide( 'fast' ); + if ( noOfSourceUnits < noOfTranslationUnits ) { + $( '.mw-tpm-sp-error__message' ).text( mw.msg( 'pm-extra-units-warning' ) ) + .show( 'fast' ); + return; + } else { + $( 'input' ).prop( 'disabled', true ); + $( '.mw-tpm-sp-instructions' ).hide( 'fast' ); + for ( i = 0; i < noOfSourceUnits; i++ ) { + content = $( '.mw-tpm-sp-unit__target' ).eq( i ).val(); + content = content.trim(); + if ( content !== '' ) { + list.push( createTranslationPage( i, content ) ); + } + } + + $.ajaxDispatcher( list, 1 ).done( function () { + $( '#action-import' ).removeClass( 'hide' ); + $( 'input' ).prop( 'disabled', false ); + $( '.mw-tpm-sp-instructions' ).text( mw.msg( 'pm-on-save-message-text' ) ).show( 'fast' ); + } ).fail( function ( errmsg ) { + $( 'input' ).prop( 'disabled', false ); + $( '.mw-tpm-sp-error__message' ).text( mw.msg( errmsg ) ).show( 'fast' ); + } ); + } + } + + /** + * Handler for 'Cancel' button click event. + */ + function cancelHandler() { + $( '.mw-tpm-sp-error__message' ).hide( 'fast' ); + $( '.mw-tpm-sp-instructions' ).hide( 'fast' ); + $( '#action-save, #action-cancel' ).addClass( 'hide' ); + $( '#action-import' ).removeClass( 'hide' ); + $( '.mw-tpm-sp-unit-listing' ).html( '' ); + } + + /** + * Handler for add new unit icon ('+') click event. Adds a translation unit + * below the current unit. + * + * @param {jQuery.Event} event + */ + function addHandler( event ) { + var nextRow, text, newUnit, targetUnit; + + nextRow = $( event.target ).closest( '.mw-tpm-sp-unit' ).next(); + targetUnit = nextRow.find( '.mw-tpm-sp-unit__target' ); + text = targetUnit.val(); + targetUnit.val( '' ); + nextRow = nextRow.next(); + text = shiftRowsDown( nextRow, text ); + if ( text ) { + newUnit = createNewUnit( '', text ); + $( '.mw-tpm-sp-unit-listing' ).append( newUnit ); + } + noOfTranslationUnits += 1; + } + + /** + * Handler for delete icon ('-') click event. Deletes the unit and shifts + * the units up by one. + * + * @param {jQuery.Event} event + */ + function deleteHandler( event ) { + var sourceText, rowUnit; + rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' ); + sourceText = rowUnit.find( '.mw-tpm-sp-unit__source' ).val(); + if ( !sourceText ) { + rowUnit.remove(); + } else { + rowUnit.find( '.mw-tpm-sp-unit__target' ).val( '' ); + shiftRowsUp( rowUnit ); + } + noOfTranslationUnits -= 1; + } + + /** + * Handler for swap icon click event. Swaps the text in the current unit + * with the text in the unit below. + * + * @param {jQuery.Event} event + */ + function swapHandler( event ) { + var rowUnit, tempText, nextVal; + rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' ); + tempText = rowUnit.find( '.mw-tpm-sp-unit__target' ).val(); + nextVal = rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val(); + rowUnit.find( '.mw-tpm-sp-unit__target' ).val( nextVal ); + rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val( tempText ); + } + + /** + * Handler for 'Import' button click event. Imports source and translation + * units and displays them. + * + * @param {jQuery.Event} e + */ + function importHandler( e ) { + var pageTitle, slashPos, titleObj, + errorBox = $( '.mw-tpm-sp-error__message' ), + messageBox = $( '.mw-tpm-sp-instructions' ); + + e.preventDefault(); + + pageTitle = $( '#title' ).val().trim(); + if ( pageTitle === '' ) { + errorBox.text( mw.msg( 'pm-pagetitle-missing' ) ).show( 'fast' ); + return; + } + + titleObj = mw.Title.newFromText( pageTitle ); + messageBox.hide( 'fast' ); + if ( titleObj === null ) { + errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).show( 'fast' ); + return; + } + + pageTitle = titleObj.getPrefixedDb(); + slashPos = pageTitle.lastIndexOf( '/' ); + + if ( slashPos === -1 ) { + errorBox.text( mw.msg( 'pm-langcode-missing' ) ).show( 'fast' ); + return; + } + + pageName = pageTitle.substring( 0, slashPos ); + langCode = pageTitle.substring( slashPos + 1 ); + + if ( pageName === '' ) { + errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).show( 'fast' ); + return; + } + + errorBox.hide( 'fast' ); + + $.when( getSourceUnits( pageName ), getFuzzyTimestamp( pageTitle ) ) + .then( function ( sourceUnits, fuzzyTimestamp ) { + noOfSourceUnits = sourceUnits.length; + splitTranslationPage( fuzzyTimestamp, pageTitle ).done( function ( translations ) { + var translationUnits = splitHeaders( translations ); + translationUnits = alignHeaders( sourceUnits, translationUnits ); + noOfTranslationUnits = translationUnits.length; + displayUnits( sourceUnits, translationUnits ); + $( '#action-save, #action-cancel' ).removeClass( 'hide' ); + $( '#action-import' ).addClass( 'hide' ); + messageBox.text( mw.msg( 'pm-on-import-message-text' ) ).show( 'fast' ); + } ); + } ); + } + + /** + * Listens to various click events + */ + function listen() { + var $listing = $( '.mw-tpm-sp-unit-listing' ); + + $( '#mw-tpm-sp-primary-form' ).submit( importHandler ); + $( '#action-import' ).click( importHandler ); + $( '#action-save' ).click( saveHandler ); + $( '#action-cancel' ).click( cancelHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--swap', swapHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--delete', deleteHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--add', addHandler ); + } + + $( listen ); + + mw.translate = mw.translate || {}; + mw.translate = $.extend( mw.translate, { + getSourceUnits: getSourceUnits, + getFuzzyTimestamp: getFuzzyTimestamp, + splitTranslationPage: splitTranslationPage, + alignHeaders: alignHeaders + } ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagepreparation.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagepreparation.js new file mode 100644 index 00000000..03c4385d --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagepreparation.js @@ -0,0 +1,426 @@ +( function () { + 'use strict'; + + /** + * Save the page with a given page name and given content to the wiki. + * + * @param {string} pageName Page title + * @param {string} pageContent Content of the page to be saved + * @return {jQuery.Promise} + */ + function savePage( pageName, pageContent ) { + var api = new mw.Api(); + + return api.postWithToken( 'csrf', { + action: 'edit', + title: pageName, + text: pageContent, + summary: $( '#pp-summary' ).val() + } ).promise(); + } + + /** + * Get the diff between the current revision and the prepared page content. + * + * @param {string} pageName Page title + * @param {string} pageContent Content of the page to be saved + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.data + */ + function getDiff( pageName, pageContent ) { + var api = new mw.Api(); + + return api.post( { + action: 'query', + prop: 'revisions', + rvprop: 'content', + rvlimit: '1', + titles: pageName, + rvdifftotext: pageContent + } ).then( function ( data ) { + var page, obj, diff; + + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + + diff = obj.revisions[ 0 ].diff[ '*' ]; + + return diff; + } ); + } + + /** + * Remove all the <translate> tags and {{translation}} templates before + * preparing the page. The tool will add them back wherever needed. + * + * @param {string} pageContent + * @return {string} + */ + function cleanupTags( pageContent ) { + pageContent = pageContent.replace( /<\/?translate>\n?/gi, '' ); + return pageContent; + } + + /** + * Add the <languages/> bar at the top of the page, if not present. + * Remove the old {{languages}} template, if present. + * + * @param {string} pageContent + * @return {string} + */ + function addLanguageBar( pageContent ) { + if ( !pageContent.match( /<languages\/>/gi ) ) { + pageContent = '<languages/>\n' + pageContent; + } + pageContent = pageContent.replace( /\{\{languages.*?\}\}/gi, '' ); + return pageContent; + } + + /** + * Add <translate> tags around Categories to make them a part of the page template + * and tag them with the {{translation}} template. + * + * @param {string} pageContent + * @return {jQuery.Promise} + */ + function doCategories( pageContent ) { + return getNamespaceAliases( 14 ).then( function ( aliases ) { + var i, aliasList, categoryRegex; + + aliases.push( 'category' ); + for ( i = 0; i < aliases.length; i++ ) { + aliases[ i ] = mw.RegExp.escape( aliases[ i ] ); + } + + aliasList = aliases.join( '|' ); + // Regex: https://regex101.com/r/sJ3gZ4/2 + categoryRegex = new RegExp( '\\[\\[((' + aliasList + ')' + + ':[^\\|]+)(\\|[^\\|]*?)?\\]\\]', 'gi' ); + pageContent = pageContent.replace( categoryRegex, '\n</translate>\n' + + '[[$1{{#translation:}}$3]]\n<translate>\n' ); + + return pageContent; + } ); + } + + /** + * Add the <translate> and </translate> tags at the start and end of the page. + * The opening tag is added immediately after the <languages/> tag. + * + * @param {string} pageContent + * @return {string} + */ + function addTranslateTags( pageContent ) { + pageContent = pageContent.replace( /(<languages\/>\n)/gi, '$1<translate>\n' ); + pageContent = pageContent + '\n</translate>'; + return pageContent; + } + + /** + * Add newlines before and after section headers. Extra newlines resulting after + * this operation are cleaned up in postPreparationCleanup() function. + * + * @param {string} pageContent + * @return {string} + */ + function addNewLines( pageContent ) { + pageContent = pageContent.replace( /^(==.*==)\n*/gm, '\n$1\n\n' ); + return pageContent; + } + + /** + * Add an anchor to a section header with the given headerText + * + * @param {string} headerText + * @param {string} pageContent + * @return {string} + */ + function addAnchor( headerText, pageContent ) { + var headerSearchRegex, anchorID, replaceAnchorRegex, + spanSearchRegex; + + anchorID = headerText.replace( ' ', '-' ).toLowerCase(); + + headerText = mw.RegExp.escape( headerText ); + // Search for the header having text as headerText + // Regex: https://regex101.com/r/fD6iL1 + headerSearchRegex = new RegExp( '(==+[ ]*' + headerText + '[ ]*==+)', 'gi' ); + // This is to ensure the tags and the anchor are added only once + + if ( pageContent.indexOf( '<span id="' + mw.html.escape( anchorID ) + '"' ) === -1 ) { + pageContent = pageContent.replace( headerSearchRegex, '</translate>\n' + + '<span id="' + mw.html.escape( anchorID ) + '"></span>\n<translate>\n$1' ); + } + + // This is to add back the tags which were removed in cleanupTags() + if ( pageContent.indexOf( '</translate>\n<span id="' + anchorID + '"' ) === -1 ) { + spanSearchRegex = new RegExp( '(<span id="' + mw.RegExp.escape( anchorID ) + '"></span>)', 'gi' ); + pageContent = pageContent.replace( spanSearchRegex, '\n</translate>\n$1\n</translate>\n' ); + } + + // Replace the link text with the anchorID defined above + // Regex: https://regex101.com/r/kB5bK3 + replaceAnchorRegex = new RegExp( '(\\[\\[#)' + headerText + '(.*\\]\\])', 'gi' ); + pageContent = pageContent.replace( replaceAnchorRegex, '$1' + + anchorID.replace( '$', '$$$' ) + '$2' ); + + return pageContent; + } + + /** + * Convert all the links into two-party form and add the 'Special:MyLanguage/' prefix + * to links in valid namespaces for the wiki. For example, [[Example]] would be converted + * to [[Special:MyLanguage/Example|Example]]. + * + * @param {string} pageContent + * @return {string} + */ + function fixInternalLinks( pageContent ) { + + var normalizeRegex, linkPrefixRegex, sectionLinksRegex, + match, searchText, namespaces, nsString; + searchText = pageContent; + + normalizeRegex = new RegExp( /\[\[(?!Category)([^|]*?)\]\]/gi ); + // First convert all links into two-party form. If a link is not having a pipe, + // add a pipe and duplicate the link text + // Regex: https://regex101.com/r/pO9nN2 + pageContent = pageContent.replace( normalizeRegex, '[[$1|$1]]' ); + + namespaces = getNamespaces(); + nsString = namespaces.join( '|' ); + // Finds all the links to sections on the same page. + // Regex: https://regex101.com/r/cX6jT3 + sectionLinksRegex = new RegExp( /\[\[#(.*?)(\|(.*?))?\]\]/gi ); + match = sectionLinksRegex.exec( searchText ); + while ( match !== null ) { + pageContent = addAnchor( match[ 1 ], pageContent ); + match = sectionLinksRegex.exec( searchText ); + } + + linkPrefixRegex = new RegExp( '\\[\\[((?:(?:special(?!:MyLanguage\\b)|' + nsString + + '):)?[^:]*?)\\]\\]', 'gi' ); + // Add the 'Special:MyLanguage/' prefix for all internal links of valid namespaces and + // mainspace. + // Regex: https://regex101.com/r/zZ9jH9 + pageContent = pageContent.replace( linkPrefixRegex, '[[Special:MyLanguage/$1]]' ); + return pageContent; + } + + /** + * Fetch all the aliases for a given namespace on the wiki. + * + * @param {number} namespaceID + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {Array} return.done.data + */ + function getNamespaceAliases( namespaceID ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + meta: 'siteinfo', + siprop: 'namespacealiases' + } ).then( function ( data ) { + var alias, aliases = []; + + for ( alias in data.query.namespacealiases ) { + if ( data.query.namespacealiases[ alias ].id === namespaceID ) { + aliases.push( data.query.namespacealiases[ alias ][ '*' ] ); + } + } + + return aliases; + } ); + } + + /** + * Add translate tags around only translatable content for files and keep everything else + * as a part of the page template. + * + * @param {string} pageContent + * @return {jQuery.Promise} + */ + function doFiles( pageContent ) { + return getNamespaceAliases( 6 ).then( function ( aliases ) { + var i, aliasList, captionFilesRegex, fileRegex; + + aliases.push( 'file' ); + + for ( i = 0; i < aliases.length; i++ ) { + aliases[ i ] = mw.RegExp.escape( aliases[ i ] ); + } + + aliasList = aliases.join( '|' ); + + // Add translate tags for files with captions + captionFilesRegex = new RegExp( '\\[\\[(' + aliasList + ')(.*\\|)(.*?)\\]\\]', 'gi' ); + pageContent = pageContent.replace( captionFilesRegex, + '</translate>\n[[$1$2<translate>$3</translate>]]\n<translate>' ); + + // Add translate tags for files without captions + fileRegex = new RegExp( '/\\[\\[((' + aliasList + ')[^\\|]*?)\\]\\]', 'gi' ); + pageContent = pageContent.replace( fileRegex, '\n</translate>[[$1]]\n<translate>' ); + + return pageContent; + } ); + } + + /** + * Keep templates outside <translate>....</translate> tags + * Does not deal with nested templates, needs manual changes. + * + * @param {string} pageContent + * @return {string} pageContent + */ + function doTemplates( pageContent ) { + var templateRegex; + // Regex: https://regex101.com/r/wA3iX0 + templateRegex = new RegExp( /^({{[\s\S]*?}})/gm ); + + pageContent = pageContent.replace( templateRegex, '</translate>\n$1\n<translate>' ); + return pageContent; + } + + /** + * Cleanup done after the page is prepared for translation by the tool. + * + * @param {string} pageContent + * @return {string} + */ + function postPreparationCleanup( pageContent ) { + // Removes any extra newlines introduced by the tool + pageContent = pageContent.replace( /\n\n+/gi, '\n\n' ); + // Removes redundant <translate> tags + pageContent = pageContent.replace( /\n<translate>(\n*?)<\/translate>/gi, '' ); + // Removes the Special:MyLanguage/ prefix for section links + pageContent = pageContent.replace( /Special:MyLanguage\/#/gi, '#' ); + return pageContent; + } + + /** + * Get the current revision for the given page. + * + * @param {string} pageName + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.value The current revision + */ + function getPageContent( pageName ) { + var obj, + api = new mw.Api(); + + return api.get( { + action: 'query', + prop: 'revisions', + rvprop: 'content', + rvlimit: '1', + titles: pageName + } ).then( function ( data ) { + var page; + + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + + return obj.revisions[ 0 ][ '*' ]; + } ); + } + + /** + * Get the list of valid namespaces for the wiki and remove unwanted + * ones from the list. + * + * @return {Array} Array of valid namespaces + */ + function getNamespaces() { + var key, namespacesObject, i, + namespaces = []; + + namespacesObject = mw.config.get( 'wgNamespaceIds' ); + for ( key in namespacesObject ) { + namespaces.push( key ); + } + + // Remove all what has been already handled somewhere else + [ '', 'category', 'category_talk', 'special', 'file', 'file_talk' ].forEach( function ( ns ) { + namespaces.splice( namespaces.indexOf( ns ), 1 ); + } ); + + for ( i = 0; i < namespaces.length; i++ ) { + namespaces[ i ] = mw.RegExp.escape( namespaces[ i ] ); + } + return namespaces; + } + + $( function () { + var pageContent, + $input = $( '#page' ); + + $( '#action-cancel' ).click( function () { + document.location.reload( true ); + } ); + + $( '#action-save' ).click( function () { + var pageName, + pageUrl = ''; + + pageName = $input.val().trim(); + savePage( pageName, pageContent ).done( function () { + pageUrl = mw.Title.newFromText( pageName ).getUrl( { action: 'edit' } ); + $( '.messageDiv' ) + .empty() + .append( mw.message( 'pp-save-message', pageUrl ).parseDom() ) + .show(); + $( '.divDiff' ).hide( 'fast' ); + $( '#action-prepare' ).show(); + $input.val( '' ); + $( '#action-save' ).hide(); + $( '#action-cancel' ).hide(); + } ); + } ); + + $( '#action-prepare' ).click( function () { + var pageName, messageDiv = $( '.messageDiv' ); + + pageName = $input.val().trim(); + messageDiv.hide(); + if ( pageName === '' ) { + // eslint-disable-next-line no-alert + alert( mw.msg( 'pp-pagename-missing' ) ); + return; + } + + $.when( getPageContent( pageName ) ).done( function ( content ) { + pageContent = content; + pageContent = pageContent.trim(); + pageContent = cleanupTags( pageContent ); + pageContent = addLanguageBar( pageContent ); + pageContent = addTranslateTags( pageContent ); + pageContent = addNewLines( pageContent ); + pageContent = fixInternalLinks( pageContent ); + pageContent = doTemplates( pageContent ); + doFiles( pageContent ).then( doCategories ).done( function ( pageContent ) { + pageContent = postPreparationCleanup( pageContent ); + pageContent = pageContent.trim(); + getDiff( pageName, pageContent ).done( function ( diff ) { + $( '.diff tbody' ).append( diff ); + $( '.divDiff' ).show( 'fast' ); + if ( diff !== '' ) { + messageDiv.text( mw.msg( 'pp-prepare-message' ) ).show(); + $( '#action-prepare' ).hide(); + $( '#action-save' ).show(); + $( '#action-cancel' ).show(); + } else { + messageDiv.text( mw.msg( 'pp-already-prepared-message' ) ).show(); + } + } ); + } ); + } ); + } ); + } ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagetranslation.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagetranslation.js new file mode 100644 index 00000000..67610f52 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagetranslation.js @@ -0,0 +1,26 @@ +/*! + * @author Santhosh Thottingal + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +( function () { + 'use strict'; + + $( function () { + $( '#wpUserLanguage' ).multiselectautocomplete( { inputbox: '#tpt-prioritylangs' } ); + + $( '#mw-content-text' ).on( 'click', '.mw-translate-jspost', function ( e ) { + var params, + uri = new mw.Uri( e.target.href ); + + params = uri.query; + params.token = mw.user.tokens.get( 'csrfToken' ); + $.post( uri.path, params ).done( function () { + location.reload(); + } ); + + e.preventDefault(); + } ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.searchtranslations.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.searchtranslations.js new file mode 100644 index 00000000..266d37c4 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.searchtranslations.js @@ -0,0 +1,397 @@ +( function () { + 'use strict'; + + var resultGroups; + + $( function () { + resultGroups = $( '.facet.groups' ).data( 'facets' ); + + $( '.tux-searchpage .button' ).click( function () { + var query = $( '.tux-searchpage .searchinputbox' ).val(), + result = lexOperators( query ), + $form = $( '.tux-searchpage form[name=searchform]' ); + + $.each( result, function ( index, value ) { + var $input = $( '<input>' ).prop( 'type', 'hidden' ), + $elem = $form.find( 'input[name=' + index + ']' ); + + if ( $elem.length ) { + $elem.val( value ); + } else { + $form.append( $input + .prop( { + value: value, + name: index + } ) + ); + } + } ); + } ); + + buildSelectedBox(); + showLanguages(); + showMessageGroups(); + + // Make the whole rows clickable + $( '.tux-searchpage .row .facet-item' ).click( function ( event ) { + window.location = $( this ).find( 'a' ).attr( 'href' ); + event.stopPropagation(); + } ); + } ); + + // ES5-compatible Chrome, IE 9+, FF 4+, or Safari 5+ has Object.keys. + // Make other old browsers happy + if ( !Object.keys ) { + Object.keys = function ( obj ) { + var keys = [], + k; + for ( k in obj ) { + if ( Object.prototype.hasOwnProperty.call( obj, k ) ) { + keys.push( k ); + } + } + return keys; + }; + } + + function showLanguages() { + var $languages, + languages, + ulslanguages = [], + currentLanguage, + resultCount, + $count, + result, + i, + selectedClasss = '', + languageCode, + quickLanguageList = [], + unique = [], + $ulsTrigger, + uri; + + $languages = $( '.facet.languages' ); + languages = $languages.data( 'facets' ); + currentLanguage = $languages.data( 'language' ); + if ( !languages ) { + return; + } + + if ( currentLanguage !== '' ) { + uri = new mw.Uri( location.href ); + uri.extend( { language: '', filter: '' } ); + addToSelectedBox( getLanguageLabel( currentLanguage ), uri.toString() ); + } + + resultCount = Object.keys( languages ).length; + quickLanguageList = quickLanguageList.concat( mw.uls.getFrequentLanguageList() ) + .concat( Object.keys( languages ) ); + + // Remove duplicates from the language list + quickLanguageList.forEach( function ( lang ) { + result = languages[ lang ]; + if ( result && unique.indexOf( lang ) === -1 ) { + unique.push( lang ); + } + } ); + + if ( currentLanguage && quickLanguageList.indexOf( currentLanguage ) >= 0 ) { + quickLanguageList = unique.splice( 0, 5 ); + if ( quickLanguageList.indexOf( currentLanguage ) === -1 ) { + quickLanguageList = quickLanguageList.concat( currentLanguage ); + } + } else { + quickLanguageList = unique.splice( 0, 6 ); + } + + quickLanguageList.sort( sortLanguages ); + + for ( i = 0; i <= quickLanguageList.length; i++ ) { + languageCode = quickLanguageList[ i ]; + result = languages[ languageCode ]; + if ( !result ) { + continue; + } + + if ( currentLanguage === languageCode ) { + selectedClasss = 'selected'; + } else { + selectedClasss = ''; + } + + $languages.append( $( '<div>' ) + .addClass( 'row facet-item' ) + .append( + $( '<span>' ) + .addClass( 'facet-name ' + selectedClasss ) + .append( $( '<a>' ) + .attr( 'href', result.url ) + .text( getLanguageLabel( languageCode ) ) + ), + $( '<span>' ) + .addClass( 'facet-count' ) + .text( result.count ) + ) + ); + } + + $.each( Object.keys( languages ), function ( index, languageCode ) { + ulslanguages[ languageCode ] = mw.config.get( 'wgTranslateLanguages' )[ languageCode ]; + } ); + + mw.translate.addExtraLanguagesToLanguageData( ulslanguages, [ 'SP' ] ); + + if ( resultCount > 6 ) { + $ulsTrigger = $( '<a>' ) + .text( '...' ) + .addClass( 'translate-search-more-languages' ); + $count = $( '<span>' ) + .addClass( 'translate-search-more-languages-info' ) + .text( mw.msg( 'translate-search-more-languages-info', resultCount - quickLanguageList.length ) ); + $languages.append( $ulsTrigger, $count ); + + $ulsTrigger.uls( { + onSelect: function ( language ) { + window.location = languages[ language ].url; + }, + compact: true, + languages: ulslanguages, + ulsPurpose: 'translate-special-searchtranslations', + top: $languages.offset().top, + showRegions: [ 'SP' ].concat( $.fn.lcd.defaults.showRegions ) + } ); + } + } + + function showMessageGroups() { + var currentGroup, + groupList, + $groups; + + $groups = $( '.facet.groups' ); + + if ( !resultGroups ) { + // No search results + return; + } + + groupList = Object.keys( resultGroups ); + listGroups( groupList, currentGroup, $groups ); + } + + function listGroups( groupList, parentGrouppath, $parent, level ) { + var i, + $grouSelectorTrigger, + selectedClass = '', + group, + groupId, + $groupRow, + uri, + maxListSize = 10, + currentGroup = $( '.facet.groups' ).data( 'group' ), + resultCount = groupList.length, + position, + groups, + options, + grouppath; + + level = level || 0; + groupList.sort( sortGroups ); + if ( level === 0 ) { + groupList = groupList.splice( 0, maxListSize ); + } + grouppath = getParameterByName( 'grouppath' ).split( '|' )[ 0 ]; + if ( currentGroup && resultGroups[ grouppath ] && + groupList.indexOf( grouppath ) < 0 && + level === 0 + ) { + // Make sure current selected group is displayed always. + groupList = groupList.concat( grouppath ); + } + groupList.sort( sortGroups ); + for ( i = 0; i < groupList.length; i++ ) { + groupId = groupList[ i ]; + group = mw.translate.findGroup( groupId, resultGroups ); + if ( !group ) { + continue; + } + + uri = new mw.Uri( location.href ); + if ( parentGrouppath !== undefined ) { + grouppath = parentGrouppath + '|' + groupId; + } else { + grouppath = groupId; + } + uri.extend( { group: groupId, grouppath: grouppath } ); + + if ( currentGroup === groupId ) { + selectedClass = 'selected'; + uri.extend( { group: '', grouppath: '' } ); + addToSelectedBox( group.label, uri.toString() ); + } else { + selectedClass = ''; + uri.extend( { group: groupId, grouppath: grouppath } ); + } + + $groupRow = $( '<div>' ) + .addClass( 'row facet-item facet-level-' + level ) + .append( + $( '<span>' ) + .addClass( 'facet-name ' + selectedClass ) + .append( $( '<a>' ) + .attr( 'href', uri.toString() ) + .text( group.label ) + ), + $( '<span>' ) + .addClass( 'facet-count' ) + .text( mw.language.convertNumber( group.count ) ) + ); + $parent.append( $groupRow ); + if ( group.groups && level < 2 ) { + listGroups( Object.keys( group.groups ), grouppath, $groupRow, level + 1 ); + } + } + + if ( resultCount > maxListSize && resultCount - groupList.length > 0 && level === 0 ) { + $grouSelectorTrigger = $( '<div>' ) + .addClass( 'rowfacet-item ' ) + .append( + $( '<a>' ) + .text( '...' ) + .addClass( 'translate-search-more-groups' ), + $( '<span>' ) + .addClass( 'translate-search-more-groups-info' ) + .text( mw.msg( 'translate-search-more-groups-info', + resultCount - groupList.length ) ) + ); + $parent.append( $grouSelectorTrigger ); + + if ( $( 'body' ).hasClass( 'rtl' ) ) { + position = { + my: 'right top', + at: 'right+90 top+40', + collision: 'none' + }; + } else { + position = { + my: 'left top', + at: 'left-90 top+40', + collision: 'none' + }; + } + options = { + language: mw.config.get( 'wgUserLanguage' ), + position: position, + onSelect: function ( group ) { + var uri = new mw.Uri( location.href ); + uri.extend( { group: group.id, grouppath: group.id } ); + location.href = uri.toString(); + }, + preventSelector: true + }; + groups = $.map( resultGroups, function ( value, index ) { + return index; + } ); + $grouSelectorTrigger.msggroupselector( + options, + groups + ); + } + } + + function lexOperators( str ) { + var string = str.split( ' ' ), + result = {}, + query = ''; + + $.each( string, function ( index, value ) { + matchOperators( value, function ( obj ) { + if ( obj === false ) { + query = query + ' ' + value; + } else { + result[ obj.operator ] = obj.value; + } + } ); + } ); + result.query = query.trim(); + + return result; + } + + function matchOperators( str, callback ) { + var matches, + counter = false, + // Add operators for different filters + operatorRegex = [ 'language', 'group', 'filter' ]; + + $.each( operatorRegex, function ( index, value ) { + var regex = new RegExp( value + ':(\\S+)', 'i' ); + if ( ( matches = regex.exec( str ) ) !== null ) { + counter = true; + callback( { + operator: value, + value: matches[ 1 ] + } ); + } + } ); + if ( !counter ) { + callback( false ); + } + } + + function sortGroups( groupIdA, groupIdB ) { + var groupAName = mw.translate.findGroup( groupIdA, resultGroups ).count, + groupBName = mw.translate.findGroup( groupIdB, resultGroups ).count; + + if ( groupAName > groupBName ) { + return -1; + } else if ( groupAName < groupBName ) { + return 1; + } + + return 0; + } + + function sortLanguages( languageA, languageB ) { + var languageNameA = mw.config.get( 'wgULSLanguages' )[ languageA ] || languageA, + languageNameB = mw.config.get( 'wgULSLanguages' )[ languageB ] || languageB; + + return languageNameA.localeCompare( languageNameB ); + } + + function getParameterByName( name ) { + var uri = new mw.Uri(); + return uri.query[ name ] || ''; + } + + function getLanguageLabel( languageCode ) { + return mw.config.get( 'wgULSLanguages' )[ languageCode ] || languageCode; + } + + // Build a selected box to show the selected items + function buildSelectedBox() { + $( '.tux-search-inputs' ) + .removeClass( 'offset-by-three' ) + .before( + $( '<div>' ) + .addClass( 'three columns tux-selectedbox' ) + ); + } + + function addToSelectedBox( label, url ) { + $( '.tux-searchpage .tux-selectedbox' ).append( $( '<div>' ) + .addClass( 'row facet-item' ) + .append( + $( '<span>' ) + .addClass( 'facet-name selected' ) + .append( $( '<a>' ) + .attr( 'href', url ) + .text( label ) + ), + $( '<span>' ) + .addClass( 'facet-count' ) + .text( 'X' ) + ) + ); + } +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.translate.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translate.js new file mode 100644 index 00000000..103cfb8c --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translate.js @@ -0,0 +1,399 @@ +( function () { + 'use strict'; + + var state = { + group: null, + language: null, + messageList: null + }; + + mw.translate = mw.translate || {}; + + mw.translate = $.extend( mw.translate, { + + /** + * Change the group that is currently displayed + * in the TUX translation editor. + * + * @param {Object} group a message group object. + */ + changeGroup: function ( group ) { + var changes; + + if ( !checkDirty() ) { + return; + } + + state.group = group.id; + + changes = { + group: group.id, + showMessage: null + + }; + + mw.translate.changeUrl( changes ); + mw.translate.updateTabLinks( changes ); + $( '.tux-editor-header .group-warning' ).empty(); + state.messageList.changeSettings( changes ); + updateGroupInformation( state ); + }, + + changeLanguage: function ( language ) { + var changes = { + language: language, + showMessage: null + }; + + state.language = language; + + mw.translate.changeUrl( changes ); + mw.translate.updateTabLinks( changes ); + $( '.tux-editor-header .group-warning' ).empty(); + state.messageList.changeSettings( changes ); + updateGroupInformation( state ); + + }, + + changeFilter: function ( filter ) { + if ( !checkDirty() ) { + return; + } + + mw.translate.changeUrl( { filter: filter, showMessage: null } ); + state.messageList.changeSettings( { filter: getActualFilter( filter ) } ); + }, + + changeUrl: function ( params, forceChange ) { + var uri = new mw.Uri( window.location.href ); + + uri.extend( params ); + + // Support removing keys from the query + $.each( params, function ( key, val ) { + if ( val === null ) { + delete uri.query[ key ]; + } + } ); + + if ( uri.toString() === window.location.href ) { + return; + } + + // If supported by the browser and requested, change the URL with + // this URI but try not to leave the page. + if ( !forceChange && history.pushState && $( '.tux-messagelist' ).length ) { + history.pushState( uri, null, uri.toString() ); + } else { + // For old browsers, just reload + window.location.href = uri.toString(); + } + }, + + /** + * Updates the navigation tabs. + * + * @param {Object} params Url parameters to update. + * @since 2013.05 + */ + updateTabLinks: function ( params ) { + $( '.tux-tab a' ).each( function () { + var $a, uri; + + $a = $( this ); + uri = new mw.Uri( $a.prop( 'href' ) ); + uri.extend( params ); + $a.prop( 'href', uri.toString() ); + } ); + } + } ); + + function getActualFilter( filter ) { + var realFilters, uri; + + realFilters = [ '!ignored' ]; + uri = new mw.Uri( window.location.href ); + if ( uri.query.optional !== '1' ) { + realFilters.push( '!optional' ); + } + if ( filter ) { + realFilters.push( filter ); + } + + return realFilters.join( '|' ); + } + + function checkDirty() { + if ( mw.translate.isDirty() ) { + // eslint-disable-next-line no-alert + return confirm( mw.msg( 'translate-js-support-unsaved-warning' ) ); + } + return true; + } + + // Returns an array of jQuery objects of rows of translated + // and proofread messages in the TUX editors. + // Used several times. + function getTranslatedMessages( $translateContainer ) { + $translateContainer = $translateContainer || $( '.ext-translate-container' ); + return $translateContainer.find( '.tux-message-item' ) + .filter( '.translated, .proofread' ); + } + + /** + * Updates all group specific stuff on the page. + * + * @param {Object} state Information about current group and language. + * @param {string} state.group Message group id. + * @param {string} state.language Language. + */ + function updateGroupInformation( state ) { + var props = 'id|priority|prioritylangs|priorityforce|description'; + + mw.translate.recentGroups.append( state.group ); + + mw.translate.getMessageGroup( state.group, props ).done( function ( group ) { + updateDescription( group ); + updateGroupWarning( group, state.language ); + } ); + } + + function updateDescription( group ) { + var + api = new mw.Api(), + $description = $( '.tux-editor-header .description' ); + + if ( group.description === null ) { + $description.empty(); + return; + } + + api.parse( group.description ).done( function ( parsedDescription ) { + // The parsed text is returned in a <p> tag, + // so it's removed here. + $description.html( parsedDescription ); + } ).fail( function () { + $description.empty(); + mw.log( 'Error parsing description for group ' + group.id ); + } ); + } + + function updateGroupWarning( group, language ) { + var preferredLanguages, headerMessage, languagesMessage, + $groupWarning = $( '.tux-editor-header .group-warning' ); + + if ( isPriorityLanguage( language, group.prioritylangs ) ) { + return; + } + + // Make a comma-separated list of preferred languages + preferredLanguages = $.map( group.prioritylangs, function ( lang ) { + // bidi isolation for language names + return '<bdi>' + $.uls.data.getAutonym( lang ) + '</bdi>'; + } ).join( ', ' ); + + headerMessage = mw.message( + group.priorityforce ? + 'tpt-discouraged-language-force-header' : + 'tpt-discouraged-language-header', + $.uls.data.getAutonym( language ) + ).parse(); + + languagesMessage = mw.message( + group.priorityforce ? + 'tpt-discouraged-language-force-content' : + 'tpt-discouraged-language-content', + preferredLanguages + ).parse(); + + $groupWarning.append( + $( '<p>' ).append( $( '<strong>' ).text( headerMessage ) ), + // html because of the <bdi> and because it's parsed + $( '<p>' ).html( languagesMessage ) + ); + } + + function isPriorityLanguage( language, priorityLanguages ) { + // Don't show priority notice if the language is message documentation. + if ( language === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) { + return true; + } + + // If no priority language is set, return early. + if ( !priorityLanguages ) { + return true; + } + + if ( priorityLanguages.indexOf( language ) !== -1 ) { + return true; + } + + return false; + } + + function setupLanguageSelector( $element ) { + var ulsOptions = { + languages: mw.config.get( 'wgTranslateLanguages' ), + showRegions: [ 'SP' ].concat( $.fn.lcd.defaults.showRegions ), + onSelect: function ( language ) { + mw.translate.changeLanguage( language ); + $element.text( $.uls.data.getAutonym( language ) ); + }, + ulsPurpose: 'translate-special-translate', + quickList: function () { + return mw.uls.getFrequentLanguageList(); + } + }; + + mw.translate.addExtraLanguagesToLanguageData( ulsOptions.languages, [ 'SP' ] ); + $element.uls( ulsOptions ); + } + + $( function () { + var $translateContainer, $hideTranslatedButton, $messageList, + filter, uri, position, offset, limit; + + $messageList = $( '.tux-messagelist' ); + state.group = $( '.tux-messagetable-loader' ).data( 'messagegroup' ); + state.language = $messageList.data( 'targetlangcode' ); + + if ( $messageList.length ) { + $messageList.messagetable(); + state.messageList = $messageList.data( 'messagetable' ); + + uri = new mw.Uri( window.location.href ); + filter = uri.query.filter; + offset = uri.query.showMessage; + if ( offset ) { + limit = uri.query.limit || 1; + // Default to no filters + filter = filter || ''; + } + + if ( filter === undefined ) { + filter = '!translated'; + } + + $( '.tux-message-selector li' ).each( function () { + var $this = $( this ); + + if ( $this.data( 'filter' ) === filter ) { + $this.addClass( 'selected' ); + } + } ); + + mw.translate.changeUrl( { + group: state.group, + language: state.language, + filter: filter, + showMessage: offset, + optional: offset ? 1 : undefined + } ); + + // Start loading messages + state.messageList.changeSettings( { + group: state.group, + language: state.language, + offset: offset, + limit: limit, + filter: getActualFilter( filter ) + } ); + } + + if ( $( 'body' ).hasClass( 'rtl' ) ) { + position = { + my: 'right top', + at: 'right+80 bottom+5' + }; + } + $( '.tux-breadcrumb__item--aggregate' ).msggroupselector( { + onSelect: mw.translate.changeGroup, + language: state.language, + position: position, + recent: mw.translate.recentGroups.get() + } ); + + updateGroupInformation( state ); + + $( '.ext-translate-language-selector .uls' ).one( 'click', function () { + var $target = $( this ); + mw.loader.using( 'ext.uls.mediawiki' ).done( function () { + setupLanguageSelector( $target ); + $target.click(); + } ); + } ); + + if ( $.fn.translateeditor ) { + // New translation editor + $( '.tux-message' ).translateeditor(); + } + + $translateContainer = $( '.ext-translate-container' ); + + if ( mw.translate.canProofread() ) { + $translateContainer.find( '.proofread-mode-button' ).removeClass( 'hide' ); + } + + $hideTranslatedButton = $translateContainer.find( '.tux-editor-clear-translated' ); + $hideTranslatedButton + .prop( 'disabled', !getTranslatedMessages( $translateContainer ).length ) + .click( function () { + getTranslatedMessages( $translateContainer ).remove(); + $( this ).prop( 'disabled', true ); + } ); + + // Message filter click handler + $translateContainer.find( '.row.tux-message-selector > li' ).on( 'click', function () { + var newFilter, + $this = $( this ); + + if ( $this.hasClass( 'more' ) ) { + return false; + } + + newFilter = $this.data( 'filter' ); + + // Remove the 'selected' class from all the items. + // Some of them could have been moved to under the "more" menu, + // so everything under .row.tux-message-selector is searched. + $translateContainer.find( '.row.tux-message-selector .selected' ) + .removeClass( 'selected' ); + mw.translate.changeFilter( newFilter ); + $this.addClass( 'selected' ); + + // TODO: this could should be in messagetable + if ( newFilter === '!translated' ) { + $hideTranslatedButton + .removeClass( 'hide' ) + .prop( 'disabled', !getTranslatedMessages( $translateContainer ).length ); + } else { + $hideTranslatedButton.addClass( 'hide' ); + } + + return false; + } ); + + // TODO: this could should be in messagetable + if ( $( '.tux-messagetable-loader' ).data( 'filter' ) === '!translated' ) { + $hideTranslatedButton.removeClass( 'hide' ); + } else { + $hideTranslatedButton.addClass( 'hide' ); + } + + // Don't let clicking the items in the "more" menu + // affect the rest of it. + $( '.row.tux-message-selector .more ul' ) + .on( 'click', function ( e ) { + e.stopPropagation(); + } ); + + $( '#tux-option-optional' ).on( 'change', function () { + var uri = new mw.Uri( window.location.href ), + checked = $( this ).prop( 'checked' ); + + mw.translate.changeUrl( { optional: checked ? 1 : 0 } ); + mw.translate.changeFilter( uri.query.filter ); + } ); + } ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstash.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstash.js new file mode 100644 index 00000000..8ceca653 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstash.js @@ -0,0 +1,250 @@ +/*! + * TranslationStash front-end logic. + * + * @author Santhosh Thottingal + * @license GPL-2.0-or-later + * @since 2013.10 + */ + +( function () { + 'use strict'; + + var userTranslations = {}, + translationStashStorage = new mw.translate.TranslationStashStorage(); + + mw.translate.canTranslate = function () { + // At this page, the new translator can translate + return true; + }; + + function getMessages( messageGroup, language, offset, limit ) { + var deferred = new mw.Api().get( { + action: 'query', + list: 'messagecollection', + mcgroup: messageGroup, + mclanguage: language, + mcoffset: offset, + mclimit: limit, + mcprop: 'definition' + } ); + + return deferred.promise(); + } + + function addMessage( message ) { + var $messageWrapper, $message, + $messageTable = $( '.tux-messagelist' ), + sourceLanguage = $messageTable.data( 'sourcelangcode' ), + sourceLanguageDir = $.uls.data.getDir( sourceLanguage ), + targetLanguage = $messageTable.data( 'targetlangcode' ), + targetLanguageDir = $.uls.data.getDir( targetLanguage ), + status = message.properties.status, + statusClass = 'tux-status-' + status, + statusMsg; + + if ( status === 'translated' ) { + // tux-status-translated + statusMsg = 'tux-status-' + status; + } + + $messageWrapper = $( '<div>' ) + .addClass( 'row tux-message' ); + + $message = $( '<div>' ) + .addClass( 'row message tux-message-item ' + status ) + .append( + $( '<div>' ) + .addClass( 'eight columns tux-list-message' ) + .append( + $( '<span>' ) + .addClass( 'tux-list-source' ) + .attr( { + lang: sourceLanguage, + dir: sourceLanguageDir + } ) + .text( message.definition ), + // Bidirectional isolation. + // This should be removed some day when proper + // unicode-bidi: isolate + // is supported everywhere + $( '<span>' ) + .html( $( 'body' ).hasClass( 'rtl' ) ? '‏' : '‎' ), + $( '<span>' ) + .addClass( 'tux-list-translation' ) + .attr( { + lang: targetLanguage, + dir: targetLanguageDir + } ) + .text( message.translation || '' ) + ), + $( '<div>' ) + .addClass( 'two columns tux-list-status text-center' ) + .append( + $( '<span>' ) + .addClass( statusClass ) + .text( statusMsg ? mw.msg( statusMsg ) : '' ) + ), + $( '<div>' ) + .addClass( 'two column tux-list-edit text-right' ) + .append( + $( '<a>' ) + .attr( { + title: mw.msg( 'translate-edit-title', message.key ) + } ) + .text( mw.msg( 'tux-edit' ) ) + ) + ); + + $messageWrapper.append( $message ); + $messageTable.append( $messageWrapper ); + // Attach translate editor to the message + $messageWrapper.translateeditor( { + message: message, + storage: translationStashStorage, + onSave: updateStats, + onSkip: function () { + var $next = this.$editTrigger.next( '.tux-message' ); + + // If there is text in the skipped message, avoid showing the + // regular "you have unsaved messages" when navigating away, + // because there is no way to get back to these messages. + this.markUnunsaved(); + + // This can happen when it's + // the last message in the translation stash + if ( !$next.length ) { + // Reload the page to get more messages + // when we get to the last one + window.location.reload(); + } + }, + onReady: function () { + this.$editor.find( '.tux-editor-skip-button' ) + .text( mw.msg( 'translate-translationstash-skip-button-label' ) ); + } + } ); + } + + /** + * Updates the translation count at the top of the message list and + * displays warning when translation limit has been reached. + * Relies on classes stash-stats and tux-status-translated. + */ + function updateStats() { + var count, + $target = $( '.stash-stats' ); + + count = $( '.tux-status-translated' ).length; + if ( count === 0 ) { + return; + } + + $target.text( mw.msg( + 'translate-translationstash-translations', + mw.language.convertNumber( count ) + ) ); + + if ( count >= mw.config.get( 'wgTranslateSandboxLimit' ) ) { + // Remove the untranslated message to disallow translation beyond the limit + $( '.tux-message' ).has( '.untranslated' ).remove(); + + // Show a message telling that the limit was reached + $( '.limit-reached' ) + .empty() + .append( $( '<h1>' ).text( mw.msg( 'tsb-limit-reached-title' ) ) ) + .append( $( '<p>' ).text( mw.msg( 'tsb-limit-reached-body' ) ) ) + .removeClass( 'hide' ); + } + } + + function loadMessages() { + var $messageTable = $( '.tux-messagelist' ), + messagegroup = '!sandbox'; + + $( '<div>' ) + .addClass( 'tux-loading-indicator' ) + .appendTo( $messageTable ); + + getMessages( messagegroup, $messageTable.data( 'targetlangcode' ) ) + .done( function ( result ) { + var untranslated, messages = result.query.messagecollection; + + $messageTable.empty(); + $.each( messages, function ( index, message ) { + message.properties = {}; + message.properties.status = 'untranslated'; + + message.group = messagegroup; + if ( userTranslations[ message.title ] ) { + message.translation = userTranslations[ message.title ].translation; + message.properties.status = 'translated'; + } + + addMessage( message ); + } ); + + // Show the editor for the first untranslated message. + untranslated = $( '.tux-message' ) + .has( '.tux-message-item.untranslated' ) + .first(); + if ( untranslated.length ) { + untranslated.data( 'translateeditor' ).show(); + } + + updateStats(); + } ).fail( function ( errorCode, response ) { + $messageTable.empty().addClass( 'error' ) + .text( 'Error: ' + errorCode + ' - ' + + ( response.error && response.error.info || 'Unknown error' ) + ); + } ); + } + + $( function () { + var $messageTable = $( '.tux-messagelist' ), + $ulsTrigger = $( '.ext-translate-language-selector > .uls' ); + + // Some links in helpers will navigate away by default. But since the messages + // will change on this page on every load, we want to avoid that. Force the + // links to open on new window/tab. + mw.hook( 'mw.translate.editor.showTranslationHelpers' ).add( function ( helpers, $editor ) { + $editor.find( 'a' ).prop( 'target', '_blank' ); + } ); + + $ulsTrigger.uls( { + ulsPurpose: 'translate-special-translationstash', + onSelect: function ( language ) { + var direction = $.uls.data.getDir( language ), + autonym = $.uls.data.getAutonym( language ); + + $ulsTrigger + .text( autonym ) + .attr( { + lang: language, + dir: direction + } ); + + $messageTable + .empty() + .data( { + targetlangcode: language, + targetlangdir: direction + } ); + + loadMessages(); + } + } ); + // Get the user translations if any(possibly from an early attempt) + // and new messages to try. + translationStashStorage.getUserTranslations() + .done( function ( translations ) { + if ( translations.translationstash.translations ) { + $.each( translations.translationstash.translations, + function ( index, translation ) { + userTranslations[ translation.title ] = translation; + } ); + } + loadMessages(); + } ); + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstats.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstats.js new file mode 100644 index 00000000..701b5fba --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstats.js @@ -0,0 +1,61 @@ +/*! + * JavaScript functions for embedding jQuery controls + * into translation notification form. + * + * @author Amir E. Aharoni + * @author Siebrand Mazeland + * @author Niklas Laxström + * @copyright Copyright © 2012-2013 Amir E. Aharoni, Siebrand Mazeland + * @license GPL-2.0-or-later + */ + +( function () { + 'use strict'; + + $( function () { + var $input = $( '#start' ), + datepicker = mw.loader.getState( 'mediawiki.widgets.datetime' ) === null; + + // Remove when MediaWiki 1.27 is no longer supported + if ( datepicker ) { + mw.loader.using( 'jquery.ui.datepicker' ).done( function () { + $input.datepicker( { + dateFormat: 'yy-mm-ddT00:00:00', + constrainInput: false, + showOn: 'focus', + changeMonth: true, + changeYear: true, + showAnim: false, + showButtonPanel: true, + maxDate: new Date() + } ).attr( 'autocomplete', 'off' ); + } ); + } else { + mw.loader.using( 'mediawiki.widgets.datetime' ).done( function () { + var widget, defaultValue, defaultDate; + + defaultDate = new Date(); + defaultDate.setDate( 1 ); + + if ( $input.val() ) { + defaultValue = new Date( $input.val() ); + } + + widget = new mw.widgets.datetime.DateTimeInputWidget( { + formatter: { + format: '${year|0}-${month|0}-${day|0}', + defaultDate: defaultDate + }, + type: 'date', + value: defaultValue, + max: new Date() + } ); + + $input.after( widget.$element ).hide(); + widget.on( 'change', function ( data ) { + $input.val( data + 'T00:00:00' ); + } ); + } ); + } + } ); +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.statsbar.js b/www/wiki/extensions/Translate/resources/js/ext.translate.statsbar.js new file mode 100644 index 00000000..aeec9314 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.statsbar.js @@ -0,0 +1,187 @@ +/*! + * Translate language statistics bar - jQuery plugin. + * + * @author Niklas Laxström + * @author Santhosh Thottingal + * @license GPL-2.0-or-later + * @since 2012-11-30 + */ + +/* + * Usage: + * $( '<div>' ).languagestatsbar( { + * language: 'fi', + * group: 'core' + * } ); + * The status bar will be rendered to the newly created div. Or use any container. + */ +( function () { + 'use strict'; + + var LanguageStatsBar = function ( container, options ) { + this.$container = $( container ); + this.group = options.group; + this.language = options.language; + this.$statsBar = null; + this.elements = null; + this.init(); + }; + + LanguageStatsBar.prototype = { + init: function () { + if ( mw.translate.languagestats[ this.language ] ) { + this.render(); + } else { + mw.translate.loadLanguageStats( this.language ) + .done( this.render.bind( this ) ); + } + }, + + /** + * Listen for the change events and update the statsbar + */ + listen: function () { + var i, + statsbar = this, + languageStats = mw.translate.languagestats[ this.language ]; + + statsbar.$statsBar.on( 'change', function ( event, to, from ) { + for ( i = 0; i < languageStats.length; i++ ) { + if ( languageStats[ i ].group === statsbar.group ) { + // Changing a proofread message does not create a new translation + if ( to === 'translated' && from !== 'proofread' ) { + languageStats[ i ].translated++; + } + if ( to === 'proofread' ) { + languageStats[ i ].proofread++; + } + if ( to === 'fuzzy' ) { + languageStats[ i ].fuzzy++; + } + + if ( from === 'fuzzy' ) { + languageStats[ i ].fuzzy--; + } + if ( from === 'proofread' ) { + languageStats[ i ].proofread--; + } + // Proofreading a message does not remove translation + if ( from === 'translated' && to !== 'proofread' ) { + languageStats[ i ].translated--; + } + break; + } + } + + // Update the stats bar + statsbar.update(); + } ); + + statsbar.$container.on( { + mouseenter: function () { + statsbar.elements.$info.removeClass( 'hide' ); + }, + mouseleave: function () { + statsbar.elements.$info.addClass( 'hide' ); + } + } ); + }, + + render: function () { + this.$statsBar = $( '<div>' ) + .addClass( 'tux-statsbar' ) + .data( 'group', this.group ); + + this.elements = { + $proofread: $( '<span>' ).addClass( 'tux-proofread' ), + $translated: $( '<span>' ).addClass( 'tux-translated' ), + $fuzzy: $( '<span>' ).addClass( 'tux-fuzzy' ), + $untranslated: $( '<span>' ).addClass( 'tux-untranslated' ), + $info: $( '<div>' ).addClass( 'tux-statsbar-info hide' ) + }; + + this.update(); + this.$statsBar.append( [ + // Append needs an array instead of an object + this.elements.$proofread, + this.elements.$translated, + this.elements.$fuzzy, + this.elements.$untranslated, + this.elements.$info + ] ); + this.$container.append( this.$statsBar ); + + this.listen(); + }, + + update: function () { + var proofread, translated, fuzzy, untranslated, + stats = this.getStatsForGroup( this.group ); + + proofread = 100 * stats.proofread / stats.total; + // Proofread messages are also translated, so remove those for + // the bar showing only translated count. + translated = stats.translated - stats.proofread; + translated = 100 * translated / stats.total; + fuzzy = 100 * stats.fuzzy / stats.total; + untranslated = 100 - proofread - translated - fuzzy; + + this.elements.$proofread[ 0 ].style.width = proofread + '%'; + this.elements.$translated[ 0 ].style.width = translated + '%'; + this.elements.$fuzzy[ 0 ].style.width = fuzzy + '%'; + this.elements.$untranslated[ 0 ].style.width = untranslated + '%'; + + translated = !translated ? 0 : translated + proofread; + proofread = !proofread ? 0 : proofread; + + if ( fuzzy ) { + this.elements.$info + .text( mw.msg( 'translate-statsbar-tooltip-with-fuzzy', + translated.toFixed(), proofread.toFixed(), + fuzzy.toFixed() ) ); + } else { + this.elements.$info + .text( mw.msg( 'translate-statsbar-tooltip', + translated.toFixed(), proofread.toFixed() ) ); + } + }, + + getStatsForGroup: function ( group ) { + var i, + languageStats = mw.translate.languagestats[ this.language ]; + + for ( i = 0; i < languageStats.length; i++ ) { + if ( languageStats[ i ].group === group ) { + return languageStats[ i ]; + } + } + + return { + proofread: 0, + total: 0, + fuzzy: 0, + translated: 0 + }; + } + }; + + /* + * languagestatsbar PLUGIN DEFINITION + */ + + $.fn.languagestatsbar = function ( options ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'languagestatsbar' ); + + if ( !data ) { + $this.data( 'languagestatsbar', ( data = new LanguageStatsBar( this, options ) ) ); + } + } ); + }; + + $.fn.languagestatsbar.Constructor = LanguageStatsBar; + + mw.translate = mw.translate || {}; + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.storage.js b/www/wiki/extensions/Translate/resources/js/ext.translate.storage.js new file mode 100644 index 00000000..85e9cb9e --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.storage.js @@ -0,0 +1,42 @@ +( function () { + 'use strict'; + + /** + * This class can save a translation into MediaWiki pages using the + * MediaWiki edit WebApi. + * + * @since 2013.10 + */ + var TranslationApiStorage = function () { + // No-op for now. Could take api module as param for example. + }; + + TranslationApiStorage.prototype = { + /** + * Save the translation. + * + * @param {string} title The title of the page including language code + * to store the translation. + * @param {string} translation The translation of the message + * @param {string} editSummary The edit summary + * @return {jQuery.Promise} + */ + save: function ( title, translation, editSummary ) { + var api = new mw.Api(); + + return api.postWithToken( 'csrf', { + action: 'edit', + title: title, + text: translation, + summary: editSummary, + // If the session expires, fail the saving instead of saving it + // as an anonymous user (if anonymous can save). + // When undefined, the parameter is not included in the request + assert: mw.user.isAnon() ? undefined : 'user' + } ); + } + }; + + mw.translate = mw.translate || {}; + mw.translate.TranslationApiStorage = TranslationApiStorage; +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.translationstashstorage.js b/www/wiki/extensions/Translate/resources/js/ext.translate.translationstashstorage.js new file mode 100644 index 00000000..17350cf2 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.translationstashstorage.js @@ -0,0 +1,57 @@ +( function () { + 'use strict'; + + /** + * This class can save translation to translation stash. + * + * @since 2013.10 + */ + var TranslationStashStorage = function () { + // No-op for now. Could take api module as param for example. + }; + + TranslationStashStorage.prototype = { + /** + * Save the translation. + * + * @param {string} title The title of the page including language code + * to store the translation. + * @param {string} translation The translation of the message + * @return {jQuery.Promise} + */ + save: function ( title, translation ) { + var api = new mw.Api(); + + return api.postWithToken( 'csrf', { + action: 'translationstash', + subaction: 'add', + title: title, + translation: translation + } ).then( function () { + // Fake normal save API + return { edit: { result: 'Success' } }; + } ); + }, + + /** + * Get the current users translations. + * + * @param {string} user User name + * @return {jQuery.Promise} + */ + getUserTranslations: function ( user ) { + var api = new mw.Api(); + + return api.postWithToken( 'csrf', { + action: 'translationstash', + subaction: 'query', + username: user + } ).promise(); + } + + }; + + mw.translate = mw.translate || {}; + mw.translate.TranslationStashStorage = TranslationStashStorage; + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.workflowselector.js b/www/wiki/extensions/Translate/resources/js/ext.translate.workflowselector.js new file mode 100644 index 00000000..b413e0dd --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.workflowselector.js @@ -0,0 +1,167 @@ +/*! + * A jQuery plugin which handles the display and change of message group + * workflow states. + * + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +( function () { + 'use strict'; + + function WorkflowSelector( container ) { + this.$container = $( container ); + + // Hide the workflow selector when clicking outside of it + $( 'html' ).on( 'click', function ( e ) { + if ( !e.isDefaultPrevented() ) { + $( container ) + .find( '.tux-workflow-status-selector' ) + .addClass( 'hide' ); + } + } ); + } + + WorkflowSelector.prototype = { + /** + * Displays the current state and selector if relevant. + * + * @param {string} groupId + * @param {string} language + * @param {string} state + */ + receiveState: function ( groupId, language, state ) { + var instance = this; + + instance.currentState = state; + instance.language = language; + + // Only if groupId changes, fetch the new states + if ( instance.groupId === groupId ) { + // But update the display + instance.display(); + return; + } + + instance.groupId = groupId; + mw.translate.getMessageGroup( groupId, 'workflowstates' ) + .done( function ( group ) { + instance.states = group.workflowstates; + instance.display(); + } ); + }, + + /** + * Calls the WebApi to change the state to a new value. + * + * @param {string} state + * @return {jQuery.Promise} + */ + changeState: function ( state ) { + var params, + api = new mw.Api(); + + params = { + action: 'groupreview', + group: this.groupId, + language: this.language, + state: state + }; + + return api.postWithToken( 'csrf', params ); + }, + + /** + * Get the text which says that the current state is X. + * + * @param {string} stateName + * @return {string} Text which should be escaped. + */ + getStateDisplay: function ( stateName ) { + return mw.msg( 'translate-workflowstatus', stateName ); + }, + + /** + * Actually constructs the DOM and displays the selector. + */ + display: function () { + var instance = this, + $display, $list; + + instance.$container.empty(); + if ( !instance.states ) { + return; + } + + $list = $( '<ul>' ) + .addClass( 'tux-dropdown-menu tux-workflow-status-selector hide' ); + + $display = $( '<div>' ) + .addClass( 'tux-workflow-status' ) + .text( mw.msg( 'translate-workflow-state-' ) ) + .click( function ( e ) { + $list.toggleClass( 'hide' ); + e.stopPropagation(); + } ); + + $.each( this.states, function ( id, data ) { + var $state; + + // Store the id also + data.id = id; + + $state = $( '<li>' ) + .data( 'state', data ) + .text( data.name ); + + if ( data.canchange && id !== instance.currentState ) { + $state.addClass( 'changeable' ); + } else { + $state.addClass( 'unchangeable' ); + } + + if ( id === instance.currentState ) { + $display.text( instance.getStateDisplay( data.name ) ); + $display.append( $( '<span>' ).addClass( 'tux-workflow-status-triangle' ) ); + $state.addClass( 'selected' ); + } + + $state.appendTo( $list ); + } ); + + $list.find( '.changeable' ).click( function () { + var state, + $this = $( this ); + + state = $this.data( 'state' ).id; + + $display.text( mw.msg( 'translate-workflow-set-doing' ) ); + $display.append( $( '<span>' ).addClass( 'tux-workflow-status-triangle' ) ); + instance.changeState( state ) + .done( function () { + instance.receiveState( instance.groupId, instance.language, state ); + } ) + .fail( function () { + // eslint-disable-next-line no-alert + alert( 'Change of state failed' ); + } ); + } ); + instance.$container.append( $display, $list ); + } + }; + + /* workflowselector jQuery definitions */ + $.fn.workflowselector = function ( groupId, language, state ) { + return this.each( function () { + var $this = $( this ), + data = $this.data( 'workflowselector' ); + + if ( !data ) { + $this.data( 'workflowselector', ( data = new WorkflowSelector( this ) ) ); + } + $this.data( 'workflowselector' ).receiveState( groupId, language, state ); + } ); + }; + $.fn.workflowselector.Constructor = WorkflowSelector; + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/jquery.ajaxdispatcher.js b/www/wiki/extensions/Translate/resources/js/jquery.ajaxdispatcher.js new file mode 100644 index 00000000..68d48e74 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/jquery.ajaxdispatcher.js @@ -0,0 +1,67 @@ +( function () { + 'use strict'; + + /** + * Call list of callbacks returning promises in serial order and returns a list of promises. + * + * @author Niklas Laxström + * + * @param {callable[]} list List of callbacks returning promises. + * @param {number} maxRetries Maximum number of times a failed promise is retried. + * @return {jQuery.Promise} + */ + function ajaxDispatcher( list, maxRetries ) { + var deferred = $.Deferred(); + + maxRetries = maxRetries || 0; + + return $.when( helper( list, maxRetries ) ) + .then( function ( promises ) { + return deferred.resolve( promises ); + } ).fail( function ( errmsg ) { + return deferred.reject( errmsg ); + } ); + } + + function helper( list, maxRetries ) { + var first, rest, retries, retrier, + deferred = $.Deferred(); + + if ( list.length === 0 ) { + deferred.resolve( [] ); + return deferred; + } + + first = list.slice( 0, 1 )[ 0 ]; + rest = list.slice( 1 ); + + retries = 0; + retrier = function ( result, promise ) { + if ( !promise.state ) { + return; + } + + if ( promise.state() === 'rejected' ) { + if ( retries < maxRetries ) { + retries += 1; + return first.call().always( retrier ); + } + } + + if ( promise.state() !== 'pending' ) { + helper( rest, maxRetries ).always( function ( promises ) { + deferred.resolve( [].concat( promise, promises ) ); + } ); + } + }; + + first.call().always( retrier ).catch( function ( errmsg ) { + return deferred.reject( errmsg ); + } ); + + return deferred; + } + + $.extend( $, { ajaxDispatcher: ajaxDispatcher } ); + +}() ); diff --git a/www/wiki/extensions/Translate/resources/js/jquery.autosize.js b/www/wiki/extensions/Translate/resources/js/jquery.autosize.js new file mode 100644 index 00000000..2de01911 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/jquery.autosize.js @@ -0,0 +1,254 @@ +/*! + Autosize 3.0.15 + license: MIT + http://www.jacklmoore.com/autosize + */ +( function ( global, factory ) { + if ( typeof define === 'function' && define.amd ) { + define( [ 'exports', 'module' ], factory ); + } else if ( typeof exports !== 'undefined' && typeof module !== 'undefined' ) { + factory( exports, module ); + } else { + var mod = { + exports: {} + }; + factory( mod.exports, mod ); + global.autosize = mod.exports; + } +}( this, function ( exports, module ) { + 'use strict'; + + var set = typeof Set === 'function' ? new Set() : ( function () { + var list = []; + + return { + has: function has( key ) { + return Boolean( list.indexOf( key ) > -1 ); + }, + add: function add( key ) { + list.push( key ); + }, + delete: function _delete( key ) { + list.splice( list.indexOf( key ), 1 ); + } }; + }() ), + + createEvent = function createEvent( name ) { + return new Event( name ); + }; + try { + new Event( 'test' ); + } catch ( e ) { + // IE does not support `new Event()` + createEvent = function ( name ) { + var evt = document.createEvent( 'Event' ); + evt.initEvent( name, true, false ); + return evt; + }; + } + + function assign( ta ) { + var _ref = arguments[ 1 ] === undefined ? {} : arguments[ 1 ], + + _ref$setOverflowX = _ref.setOverflowX, + setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX, + _ref$setOverflowY = _ref.setOverflowY, + setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY; + + if ( !ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has( ta ) ) { return; } + + var heightOffset = null, + overflowY = null, + clientWidth = ta.clientWidth; + + function init() { + var style = window.getComputedStyle( ta, null ); + + overflowY = style.overflowY; + + if ( style.resize === 'vertical' ) { + ta.style.resize = 'none'; + } else if ( style.resize === 'both' ) { + ta.style.resize = 'horizontal'; + } + + if ( style.boxSizing === 'content-box' ) { + heightOffset = -( parseFloat( style.paddingTop ) + parseFloat( style.paddingBottom ) ); + } else { + heightOffset = parseFloat( style.borderTopWidth ) + parseFloat( style.borderBottomWidth ); + } + // Fix when a textarea is not on document body and heightOffset is Not a Number + if ( isNaN( heightOffset ) ) { + heightOffset = 0; + } + + update(); + } + + function changeOverflow( value ) { + { + // Chrome/Safari-specific fix: + // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space + // made available by removing the scrollbar. The following forces the necessary text reflow. + var width = ta.style.width; + ta.style.width = '0px'; + // Force reflow: + /* jshint ignore:start */ + ta.offsetWidth; + /* jshint ignore:end */ + ta.style.width = width; + } + + overflowY = value; + + if ( setOverflowY ) { + ta.style.overflowY = value; + } + + resize(); + } + + function resize() { + var htmlTop = window.pageYOffset, + bodyTop = document.body.scrollTop, + originalHeight = ta.style.height; + + ta.style.height = 'auto'; + + var endHeight = ta.scrollHeight + heightOffset; + + if ( ta.scrollHeight === 0 ) { + // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM. + ta.style.height = originalHeight; + return; + } + + ta.style.height = endHeight + 'px'; + + // used to check if an update is actually necessary on window.resize + clientWidth = ta.clientWidth; + + // prevents scroll-position jumping + document.documentElement.scrollTop = htmlTop; + document.body.scrollTop = bodyTop; + } + + function update() { + var startHeight = ta.style.height; + + resize(); + + var style = window.getComputedStyle( ta, null ); + + if ( style.height !== ta.style.height ) { + if ( overflowY !== 'visible' ) { + changeOverflow( 'visible' ); + } + } else { + if ( overflowY !== 'hidden' ) { + changeOverflow( 'hidden' ); + } + } + + if ( startHeight !== ta.style.height ) { + var evt = createEvent( 'autosize:resized' ); + ta.dispatchEvent( evt ); + } + } + + var pageResize = function pageResize() { + if ( ta.clientWidth !== clientWidth ) { + update(); + } + }, + + destroy = ( function ( style ) { + window.removeEventListener( 'resize', pageResize, false ); + ta.removeEventListener( 'input', update, false ); + ta.removeEventListener( 'keyup', update, false ); + ta.removeEventListener( 'autosize:destroy', destroy, false ); + ta.removeEventListener( 'autosize:update', update, false ); + set.delete( ta ); + + Object.keys( style ).forEach( function ( key ) { + ta.style[ key ] = style[ key ]; + } ); + } ).bind( ta, { + height: ta.style.height, + resize: ta.style.resize, + overflowY: ta.style.overflowY, + overflowX: ta.style.overflowX, + wordWrap: ta.style.wordWrap } ); + + ta.addEventListener( 'autosize:destroy', destroy, false ); + + // IE9 does not fire onpropertychange or oninput for deletions, + // so binding to onkeyup to catch most of those events. + // There is no way that I know of to detect something like 'cut' in IE9. + if ( 'onpropertychange' in ta && 'oninput' in ta ) { + ta.addEventListener( 'keyup', update, false ); + } + + window.addEventListener( 'resize', pageResize, false ); + ta.addEventListener( 'input', update, false ); + ta.addEventListener( 'autosize:update', update, false ); + set.add( ta ); + + if ( setOverflowX ) { + ta.style.overflowX = 'hidden'; + ta.style.wordWrap = 'break-word'; + } + + init(); + } + + function destroy( ta ) { + if ( !( ta && ta.nodeName && ta.nodeName === 'TEXTAREA' ) ) { return; } + var evt = createEvent( 'autosize:destroy' ); + ta.dispatchEvent( evt ); + } + + function update( ta ) { + if ( !( ta && ta.nodeName && ta.nodeName === 'TEXTAREA' ) ) { return; } + var evt = createEvent( 'autosize:update' ); + ta.dispatchEvent( evt ); + } + + var autosize = null; + + // Do nothing in Node.js environment and IE8 (or lower) + if ( typeof window === 'undefined' || typeof window.getComputedStyle !== 'function' ) { + autosize = function ( el ) { + return el; + }; + autosize.destroy = function ( el ) { + return el; + }; + autosize.update = function ( el ) { + return el; + }; + } else { + autosize = function ( el, options ) { + if ( el ) { + Array.prototype.forEach.call( el.length ? el : [ el ], function ( x ) { + return assign( x, options ); + } ); + } + return el; + }; + autosize.destroy = function ( el ) { + if ( el ) { + Array.prototype.forEach.call( el.length ? el : [ el ], destroy ); + } + return el; + }; + autosize.update = function ( el ) { + if ( el ) { + Array.prototype.forEach.call( el.length ? el : [ el ], update ); + } + return el; + }; + } + + module.exports = autosize; +} ) ); diff --git a/www/wiki/extensions/Translate/resources/js/jquery.textchange.js b/www/wiki/extensions/Translate/resources/js/jquery.textchange.js new file mode 100644 index 00000000..65886c43 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/jquery.textchange.js @@ -0,0 +1,44 @@ +/*! + * Trigger a textchange event on text change in input fields. + * And make it cross browser compatible. + * + * @author Santhosh Thottingal, 2013 + * @see https://gist.github.com/mkelly12/424774 + */ +( function () { + 'use strict'; + + $.event.special.textchange = { + + setup: function () { + $( this ) + .data( 'lastValue', $( this ).val() ) + .on( 'keyup.textchange', $.event.special.textchange.handler ) + .on( 'cut.textchange paste.textchange input.textchange', $.event.special.textchange.delayedHandler ); + }, + + teardown: function () { + $( this ).off( '.textchange' ); + }, + + handler: function () { + $.event.special.textchange.triggerIfChanged( $( this ) ); + }, + + delayedHandler: function () { + var element = $( this ); + setTimeout( function () { + $.event.special.textchange.triggerIfChanged( element ); + }, 25 ); + }, + + triggerIfChanged: function ( element ) { + var current = element.val(); + if ( current !== element.data( 'lastValue' ) ) { + element.trigger( 'textchange', [ element.data( 'lastValue' ) ] ); + element.data( 'lastValue', current ); + } + } + }; + +}() ); |