summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/resources
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/resources
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/resources')
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.css8
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.dropdownmenu.css13
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.editor.css460
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.css167
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.groupselector.less143
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.legacy.css65
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.loader.css51
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.css292
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.messagetable.less283
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.messagewebimporter.css3
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.navitoggle.css56
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.pagemode.css110
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.proofread.css179
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.quickedit.css108
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.aggregategroups.css53
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.languagestats.css21
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.managegroups.css20
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.managetranslatorsandbox.css284
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.pagemigration.css76
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.pagepreparation.css11
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.pagetranslation.css27
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.searchtranslations.css165
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.supportedlanguages.css21
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.translate.css220
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.special.translationstash.css98
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.statsbar.css38
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.statstable.less63
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.tabgroup.css8
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.tag.languages.css71
-rw-r--r--www/wiki/extensions/Translate/resources/css/ext.translate.workflowselector.css53
-rw-r--r--www/wiki/extensions/Translate/resources/images/action-edit.pngbin0 -> 269 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/action-edit.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/add.pngbin0 -> 653 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/add.svg23
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-small.pngbin0 -> 229 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-small.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-sprite-ltr.pngbin0 -> 5606 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-sprite-ltr.svg15
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-sprite-rtl.pngbin0 -> 5829 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/check-sprite-rtl.svg15
-rw-r--r--www/wiki/extensions/Translate/resources/images/close.pngbin0 -> 180 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/close.svg5
-rw-r--r--www/wiki/extensions/Translate/resources/images/contract-ltr.pngbin0 -> 260 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/contract-ltr.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/contract-rtl.pngbin0 -> 253 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/contract-rtl.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/edit-mark.pngbin0 -> 313 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/edit-mark.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/expand-ltr.pngbin0 -> 288 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/expand-ltr.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/expand-rtl.pngbin0 -> 286 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/expand-rtl.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-clock.pngbin0 -> 269 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-clock.svg5
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-flag.pngbin0 -> 133 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-flag.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-page-tick.pngbin0 -> 288 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-page-tick.svg40
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-page.pngbin0 -> 193 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-page.svg42
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-pen.pngbin0 -> 217 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-pen.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-tick.pngbin0 -> 254 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/label-tick.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/loading.gifbin0 -> 10771 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/loading.svg7
-rw-r--r--www/wiki/extensions/Translate/resources/images/outdated-ltr.pngbin0 -> 617 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/outdated-ltr.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/outdated-rtl.pngbin0 -> 839 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/outdated-rtl.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/paste.pngbin0 -> 510 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/paste.svg7
-rw-r--r--www/wiki/extensions/Translate/resources/images/plus_darkgray.pngbin0 -> 669 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/plus_darkgray.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/prog-1.pngbin0 -> 236 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/prog-2.pngbin0 -> 317 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/prog-3.pngbin0 -> 315 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/prog-4.pngbin0 -> 308 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/prog-5.pngbin0 -> 236 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/project.pngbin0 -> 969 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/project.svg13
-rw-r--r--www/wiki/extensions/Translate/resources/images/remove.pngbin0 -> 637 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/remove.svg23
-rw-r--r--www/wiki/extensions/Translate/resources/images/search.pngbin0 -> 582 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/search.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/switch.pngbin0 -> 5456 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/switch.svg5
-rw-r--r--www/wiki/extensions/Translate/resources/images/translate-ltr.pngbin0 -> 441 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/translate-ltr.svg5
-rw-r--r--www/wiki/extensions/Translate/resources/images/translate-rtl.pngbin0 -> 578 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/translate-rtl.svg5
-rw-r--r--www/wiki/extensions/Translate/resources/images/trash_darkgray.pngbin0 -> 745 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/trash_darkgray.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/user-small.pngbin0 -> 215 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/user-small.svg7
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-list-hi.pngbin0 -> 127 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-list-hi.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-list.pngbin0 -> 146 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-list.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-page-hi.pngbin0 -> 151 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-page-hi.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-page.pngbin0 -> 179 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-page.svg6
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-proofread-hi.pngbin0 -> 208 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-proofread-hi.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-proofread.pngbin0 -> 269 bytes
-rw-r--r--www/wiki/extensions/Translate/resources/images/view-proofread.svg4
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.base.js192
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.dropdownmenu.js12
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.editor.helpers.js542
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.editor.js1324
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.editor.shortcuts.js71
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js633
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.hooks.js37
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.messagetable.js905
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.multiselectautocomplete.js95
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.navitoggle.js41
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.pagemode.js136
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.pagetranslation.uls.js15
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.parsers.js81
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.proofread.js282
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.quickedit.js402
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.recentgroups.js31
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.selecttoinput.js27
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.aggregategroups.js364
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.importtranslations.js20
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.languagestats.js136
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.managetranslatorsandbox.js755
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.operatorsuggest.js39
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js523
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.pagepreparation.js426
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.pagetranslation.js26
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.searchtranslations.js397
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.translate.js399
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstash.js250
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.special.translationstats.js61
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.statsbar.js187
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.storage.js42
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.translationstashstorage.js57
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.workflowselector.js167
-rw-r--r--www/wiki/extensions/Translate/resources/js/jquery.ajaxdispatcher.js67
-rw-r--r--www/wiki/extensions/Translate/resources/js/jquery.autosize.js254
-rw-r--r--www/wiki/extensions/Translate/resources/js/jquery.textchange.js44
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
new file mode 100644
index 00000000..362cb6bd
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/action-edit.png
Binary files differ
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
new file mode 100644
index 00000000..b2863b4e
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/add.png
Binary files differ
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
new file mode 100644
index 00000000..7217f8d6
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/check-small.png
Binary files differ
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
new file mode 100644
index 00000000..9a7300e2
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/check-sprite-ltr.png
Binary files differ
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
new file mode 100644
index 00000000..17de9755
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/check-sprite-rtl.png
Binary files differ
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
new file mode 100644
index 00000000..84631c3d
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/close.png
Binary files differ
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
new file mode 100644
index 00000000..11296ee3
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/contract-ltr.png
Binary files differ
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
new file mode 100644
index 00000000..4c6d9acf
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/contract-rtl.png
Binary files differ
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
new file mode 100644
index 00000000..c5e0cd9f
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/edit-mark.png
Binary files differ
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
new file mode 100644
index 00000000..1d1a8b8c
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/expand-ltr.png
Binary files differ
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
new file mode 100644
index 00000000..00e8f3e6
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/expand-rtl.png
Binary files differ
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
new file mode 100644
index 00000000..805dd5ed
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-clock.png
Binary files differ
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
new file mode 100644
index 00000000..8d5b09d6
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-flag.png
Binary files differ
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
new file mode 100644
index 00000000..d75a1e7a
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-page-tick.png
Binary files differ
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
new file mode 100644
index 00000000..69553463
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-page.png
Binary files differ
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
new file mode 100644
index 00000000..8cd99c1d
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-pen.png
Binary files differ
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
new file mode 100644
index 00000000..e993469e
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/label-tick.png
Binary files differ
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
new file mode 100644
index 00000000..2212db95
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/loading.gif
Binary files differ
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
new file mode 100644
index 00000000..64674cee
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/outdated-ltr.png
Binary files differ
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
new file mode 100644
index 00000000..fedea4e3
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/outdated-rtl.png
Binary files differ
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
new file mode 100644
index 00000000..8b8b61ce
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/paste.png
Binary files differ
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
new file mode 100644
index 00000000..2cca7c17
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/plus_darkgray.png
Binary files differ
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
new file mode 100644
index 00000000..8788c993
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/prog-1.png
Binary files differ
diff --git a/www/wiki/extensions/Translate/resources/images/prog-2.png b/www/wiki/extensions/Translate/resources/images/prog-2.png
new file mode 100644
index 00000000..1e8ff84e
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/prog-2.png
Binary files differ
diff --git a/www/wiki/extensions/Translate/resources/images/prog-3.png b/www/wiki/extensions/Translate/resources/images/prog-3.png
new file mode 100644
index 00000000..bfba1464
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/prog-3.png
Binary files differ
diff --git a/www/wiki/extensions/Translate/resources/images/prog-4.png b/www/wiki/extensions/Translate/resources/images/prog-4.png
new file mode 100644
index 00000000..132ee756
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/prog-4.png
Binary files differ
diff --git a/www/wiki/extensions/Translate/resources/images/prog-5.png b/www/wiki/extensions/Translate/resources/images/prog-5.png
new file mode 100644
index 00000000..8b86fbb4
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/prog-5.png
Binary files differ
diff --git a/www/wiki/extensions/Translate/resources/images/project.png b/www/wiki/extensions/Translate/resources/images/project.png
new file mode 100644
index 00000000..0d1abc38
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/project.png
Binary files differ
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
new file mode 100644
index 00000000..672e3586
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/remove.png
Binary files differ
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
new file mode 100644
index 00000000..97e30d24
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/search.png
Binary files differ
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
new file mode 100644
index 00000000..01a225ae
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/switch.png
Binary files differ
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
new file mode 100644
index 00000000..d52c25c1
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/translate-ltr.png
Binary files differ
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
new file mode 100644
index 00000000..31466b2c
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/translate-rtl.png
Binary files differ
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
new file mode 100644
index 00000000..8e3eaca6
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/trash_darkgray.png
Binary files differ
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
new file mode 100644
index 00000000..f8a4cbe9
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/user-small.png
Binary files differ
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
new file mode 100644
index 00000000..4822d7a6
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-list-hi.png
Binary files differ
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
new file mode 100644
index 00000000..21872a06
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-list.png
Binary files differ
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
new file mode 100644
index 00000000..4d0ef7cd
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-page-hi.png
Binary files differ
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
new file mode 100644
index 00000000..e348f639
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-page.png
Binary files differ
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
new file mode 100644
index 00000000..78650b79
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-proofread-hi.png
Binary files differ
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
new file mode 100644
index 00000000..eabdbab7
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/images/view-proofread.png
Binary files differ
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' ) ? '&rlm;' : '&lrm;' ),
+ $( '<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, '&#160;' )
+ .replace( / $/gm, '&#160;' )
+ .replace( / {2}/g, '&#160; ' )
+ .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' ) ? '&rlm;' : '&lrm;' ),
+ $( '<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 );
+ }
+ }
+ };
+
+}() );