summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/LocalisationUpdate
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/LocalisationUpdate
first commit
Diffstat (limited to 'www/wiki/extensions/LocalisationUpdate')
-rw-r--r--www/wiki/extensions/LocalisationUpdate/Autoload.php28
-rw-r--r--www/wiki/extensions/LocalisationUpdate/CODE_OF_CONDUCT.md1
-rw-r--r--www/wiki/extensions/LocalisationUpdate/COPYING339
-rw-r--r--www/wiki/extensions/LocalisationUpdate/Gruntfile.js28
-rw-r--r--www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.class.php65
-rw-r--r--www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.php15
-rw-r--r--www/wiki/extensions/LocalisationUpdate/QuickArrayReader.php214
-rw-r--r--www/wiki/extensions/LocalisationUpdate/README34
-rw-r--r--www/wiki/extensions/LocalisationUpdate/Updater.php195
-rw-r--r--www/wiki/extensions/LocalisationUpdate/composer.json55
-rw-r--r--www/wiki/extensions/LocalisationUpdate/extension.json55
-rw-r--r--www/wiki/extensions/LocalisationUpdate/fetcher/Fetcher.php28
-rw-r--r--www/wiki/extensions/LocalisationUpdate/fetcher/FetcherFactory.php25
-rw-r--r--www/wiki/extensions/LocalisationUpdate/fetcher/FileSystemFetcher.php37
-rw-r--r--www/wiki/extensions/LocalisationUpdate/fetcher/GitHubFetcher.php41
-rw-r--r--www/wiki/extensions/LocalisationUpdate/fetcher/HttpFetcher.php42
-rw-r--r--www/wiki/extensions/LocalisationUpdate/finder/Finder.php109
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/af.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ar.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ast.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ba.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/bar.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/be-tarask.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/bg.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/bn.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/br.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/bs.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ca.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ce.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/cs.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/cy.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/da.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/de.json11
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/dsb.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/el.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/en.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/eo.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/es.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/et.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/eu.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/fa.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/fi.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/fr.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/gl.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/gsw.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/gu.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/he.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/hil.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/hr.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/hsb.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/hu.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ia.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/id.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ilo.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/it.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ja.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/km.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ko.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ksh.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/lb.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/lij.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/mk.json9
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ml.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ms.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/nb.json10
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/nl.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/nn.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/oc.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/pl.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/pms.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/pt-br.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/pt.json10
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/qqq.json12
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ro.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/roa-tara.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ru.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/sk.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/sr-ec.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/sr-el.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/su.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/sv.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/ta.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/te.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/tl.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/tr.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/uk.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/vep.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/vi.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/wa.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/yi.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/yue.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/zh-hans.json8
-rw-r--r--www/wiki/extensions/LocalisationUpdate/i18n/zh-hant.json10
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/LocalisationUpdate.php73
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/QuickArrayReader.php214
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/Updater.php204
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/fetcher/Fetcher.php30
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/fetcher/FetcherFactory.php25
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/fetcher/FileSystemFetcher.php47
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/fetcher/GitHubFetcher.php47
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/fetcher/HttpFetcher.php49
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/finder/Finder.php124
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/reader/JSONReader.php37
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/reader/PHPReader.php61
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/reader/Reader.php21
-rw-r--r--www/wiki/extensions/LocalisationUpdate/includes/reader/ReaderFactory.php38
-rw-r--r--www/wiki/extensions/LocalisationUpdate/phpcs.xml15
-rw-r--r--www/wiki/extensions/LocalisationUpdate/reader/JSONReader.php32
-rw-r--r--www/wiki/extensions/LocalisationUpdate/reader/PHPReader.php56
-rw-r--r--www/wiki/extensions/LocalisationUpdate/reader/Reader.php21
-rw-r--r--www/wiki/extensions/LocalisationUpdate/reader/ReaderFactory.php38
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phan/config.php3
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phpunit/Makefile12
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php89
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php86
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php42
-rw-r--r--www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php43
-rw-r--r--www/wiki/extensions/LocalisationUpdate/update.php93
118 files changed, 3441 insertions, 0 deletions
diff --git a/www/wiki/extensions/LocalisationUpdate/Autoload.php b/www/wiki/extensions/LocalisationUpdate/Autoload.php
new file mode 100644
index 00000000..8be7124a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/Autoload.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+$dir = __DIR__;
+
+$GLOBALS['wgAutoloadClasses']['LocalisationUpdate'] = "$dir/LocalisationUpdate.class.php";
+$GLOBALS['wgAutoloadClasses']['LU_Updater'] = "$dir/Updater.php";
+$GLOBALS['wgAutoloadClasses']['QuickArrayReader'] = "$dir/QuickArrayReader.php";
+
+# fetcher
+$GLOBALS['wgAutoloadClasses']['LU_Fetcher'] = "$dir/fetcher/Fetcher.php";
+$GLOBALS['wgAutoloadClasses']['LU_FetcherFactory'] = "$dir/fetcher/FetcherFactory.php";
+$GLOBALS['wgAutoloadClasses']['LU_FileSystemFetcher'] = "$dir/fetcher/FileSystemFetcher.php";
+$GLOBALS['wgAutoloadClasses']['LU_GitHubFetcher'] = "$dir/fetcher/GitHubFetcher.php";
+$GLOBALS['wgAutoloadClasses']['LU_HttpFetcher'] = "$dir/fetcher/HttpFetcher.php";
+
+# finder
+$GLOBALS['wgAutoloadClasses']['LU_Finder'] = "$dir/finder/Finder.php";
+
+# reader
+$GLOBALS['wgAutoloadClasses']['LU_JSONReader'] = "$dir/reader/JSONReader.php";
+$GLOBALS['wgAutoloadClasses']['LU_PHPReader'] = "$dir/reader/PHPReader.php";
+$GLOBALS['wgAutoloadClasses']['LU_Reader'] = "$dir/reader/Reader.php";
+$GLOBALS['wgAutoloadClasses']['LU_ReaderFactory'] = "$dir/reader/ReaderFactory.php";
diff --git a/www/wiki/extensions/LocalisationUpdate/CODE_OF_CONDUCT.md b/www/wiki/extensions/LocalisationUpdate/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..d8e5d087
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/CODE_OF_CONDUCT.md
@@ -0,0 +1 @@
+The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Code_of_Conduct).
diff --git a/www/wiki/extensions/LocalisationUpdate/COPYING b/www/wiki/extensions/LocalisationUpdate/COPYING
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/www/wiki/extensions/LocalisationUpdate/Gruntfile.js b/www/wiki/extensions/LocalisationUpdate/Gruntfile.js
new file mode 100644
index 00000000..26ca0c16
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/Gruntfile.js
@@ -0,0 +1,28 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
+ grunt.loadNpmTasks( 'grunt-jsonlint' );
+ grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+
+ var conf = grunt.file.readJSON( 'extension.json' );
+ grunt.initConfig( {
+ banana: conf.MessagesDirs,
+ jshint: {
+ all: [
+ '**/*.js',
+ '!node_modules/**',
+ '!vendor/**'
+ ]
+ },
+ jsonlint: {
+ all: [
+ '**/*.json',
+ '!node_modules/**',
+ '!vendor/**'
+ ]
+ }
+ } );
+
+ grunt.registerTask( 'test', [ 'jsonlint', 'banana', 'jshint' ] );
+ grunt.registerTask( 'default', 'test' );
+};
diff --git a/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.class.php b/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.class.php
new file mode 100644
index 00000000..af020db0
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.class.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Class for localization update hooks and static methods.
+ */
+class LocalisationUpdate {
+ /**
+ * Hook: LocalisationCacheRecacheFallback
+ */
+ public static function onRecacheFallback( LocalisationCache $lc, $code, array &$cache ) {
+ $dir = self::getDirectory();
+ if ( !$dir ) {
+ return true;
+ }
+
+ $fileName = "$dir/" . self::getFilename( $code );
+ if ( is_readable( $fileName ) ) {
+ $data = FormatJson::decode( file_get_contents( $fileName ), true );
+ $cache['messages'] = array_merge( $cache['messages'], $data );
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook: LocalisationCacheRecache
+ */
+ public static function onRecache( LocalisationCache $lc, $code, array &$cache ) {
+ $dir = self::getDirectory();
+ if ( !$dir ) {
+ return true;
+ }
+
+ $codeSequence = array_merge( [ $code ], $cache['fallbackSequence'] );
+ foreach ( $codeSequence as $csCode ) {
+ $fileName = "$dir/" . self::getFilename( $csCode );
+ $cache['deps'][] = new FileDependency( $fileName );
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a directory where updated translations are stored.
+ *
+ * @return string|false False if not configured.
+ * @since 1.1
+ */
+ public static function getDirectory() {
+ global $wgLocalisationUpdateDirectory, $wgCacheDirectory;
+
+ return $wgLocalisationUpdateDirectory ?: $wgCacheDirectory;
+ }
+
+ /**
+ * Returns a filename where updated translations are stored.
+ *
+ * @param string $language Language tag
+ * @return string
+ * @since 1.1
+ */
+ public static function getFilename( $language ) {
+ return "l10nupdate-$language.json";
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.php b/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.php
new file mode 100644
index 00000000..45899688
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/LocalisationUpdate.php
@@ -0,0 +1,15 @@
+<?php
+
+if ( function_exists( 'wfLoadExtension' ) ) {
+ wfLoadExtension( 'LocalisationUpdate' );
+ // Keep i18n globals so mergeMessageFileList.php doesn't break
+ $GLOBALS['wgMessagesDirs']['LocalisationUpdate'] = __DIR__ . '/i18n';
+ wfWarn(
+ 'Deprecated PHP entry point used for LocalisationUpdate extension. ' .
+ 'Please use wfLoadExtension instead, ' .
+ 'see https://www.mediawiki.org/wiki/Extension_registration for more details.'
+ );
+ return;
+} else {
+ die( 'This version of the LocalisationUpdate extension requires MediaWiki 1.25+' );
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/QuickArrayReader.php b/www/wiki/extensions/LocalisationUpdate/QuickArrayReader.php
new file mode 100644
index 00000000..d8b7972b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/QuickArrayReader.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * Quickie parser class that can happily read the subset of PHP we need
+ * for our localization arrays safely.
+ *
+ * Still an order of magnitude slower than eval().
+ */
+class QuickArrayReader {
+ private $vars = [];
+
+ /**
+ * @param $string string
+ */
+ function __construct( $string ) {
+ $scalarTypes = [
+ T_LNUMBER => true,
+ T_DNUMBER => true,
+ T_STRING => true,
+ T_CONSTANT_ENCAPSED_STRING => true,
+ ];
+ $skipTypes = [
+ T_WHITESPACE => true,
+ T_COMMENT => true,
+ T_DOC_COMMENT => true,
+ ];
+ $tokens = token_get_all( $string );
+ $count = count( $tokens );
+ for ( $i = 0; $i < $count; ) {
+ while ( isset( $skipTypes[$tokens[$i][0]] ) ) {
+ $i++;
+ }
+ switch ( $tokens[$i][0] ) {
+ case T_OPEN_TAG:
+ $i++;
+ continue;
+ case T_VARIABLE:
+ // '$messages' -> 'messages'
+ $varname = trim( substr( $tokens[$i][1], 1 ) );
+ $varindex = null;
+
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === '[' ) {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $varindex = $this->parseScalar( $tokens[$i] );
+ } else {
+ throw $this->except( $tokens[$i], 'scalar index' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] !== ']' ) {
+ throw $this->except( $tokens[$i], ']' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ }
+
+ if ( $tokens[$i] !== '=' ) {
+ throw $this->except( $tokens[$i], '=' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $buildval = $this->parseScalar( $tokens[$i] );
+ } elseif ( $tokens[$i][0] === T_ARRAY ) {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ if ( $tokens[$i] !== '(' ) {
+ throw $this->except( $tokens[$i], '(' );
+ }
+ $buildval = [];
+ do {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === ')' ) {
+ break;
+ }
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $key = $this->parseScalar( $tokens[$i] );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i][0] !== T_DOUBLE_ARROW ) {
+ throw $this->except( $tokens[$i], '=>' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $val = $this->parseScalar( $tokens[$i] );
+ }
+ wfSuppressWarnings();
+ $buildval[$key] = $val;
+ wfRestoreWarnings();
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === ',' ) {
+ continue;
+ } elseif ( $tokens[$i] === ')' ) {
+ break;
+ } else {
+ throw $this->except( $tokens[$i], ', or )' );
+ }
+ } while ( true );
+ } else {
+ throw $this->except( $tokens[$i], 'scalar or array' );
+ }
+ if ( is_null( $varindex ) ) {
+ $this->vars[$varname] = $buildval;
+ } else {
+ wfSuppressWarnings();
+ $this->vars[$varname][$varindex] = $buildval;
+ wfRestoreWarnings();
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ if ( $tokens[$i] !== ';' ) {
+ throw $this->except( $tokens[$i], ';' );
+ }
+ $i++;
+ break;
+ default:
+ throw $this->except( $tokens[$i], 'open tag, whitespace, or variable.' );
+ }
+ }
+ }
+
+ /**
+ * @param $got string
+ * @param $expected string
+ * @return Exception
+ */
+ private function except( $got, $expected ) {
+ if ( is_array( $got ) ) {
+ $got = token_name( $got[0] ) . " ('" . $got[1] . "')";
+ } else {
+ $got = "'" . $got . "'";
+ }
+
+ return new Exception( "Expected $expected, got $got" );
+ }
+
+ /**
+ * Parse a scalar value in PHP
+ *
+ * @param $token string
+ *
+ * @return mixed Parsed value
+ */
+ function parseScalar( $token ) {
+ if ( is_array( $token ) ) {
+ $str = $token[1];
+ } else {
+ $str = $token;
+ }
+ if ( $str !== '' && $str[0] == '\'' ) {
+ // Single-quoted string
+ // @fixme trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ return strtr( substr( trim( $str ), 1, -1 ),
+ [ '\\\'' => '\'', '\\\\' => '\\' ] );
+ }
+
+ wfSuppressWarnings();
+ if ( $str !== '' && $str[0] == '"' ) {
+ // Double-quoted string
+ // @fixme trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ wfRestoreWarnings();
+ return stripcslashes( substr( trim( $str ), 1, -1 ) );
+ }
+ wfRestoreWarnings();
+
+ if ( substr( $str, 0, 4 ) === 'true' ) {
+ return true;
+ }
+
+ if ( substr( $str, 0, 5 ) === 'false' ) {
+ return false;
+ }
+
+ if ( substr( $str, 0, 4 ) === 'null' ) {
+ return null;
+ }
+
+ // Must be some kind of numeric value, so let PHP's weak typing
+ // be useful for a change
+ return $str;
+ }
+
+ /**
+ * @param $varname string
+ * @return null|string
+ */
+ function getVar( $varname ) {
+ if ( isset( $this->vars[$varname] ) ) {
+ return $this->vars[$varname];
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/README b/www/wiki/extensions/LocalisationUpdate/README
new file mode 100644
index 00000000..c5e2f72e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/README
@@ -0,0 +1,34 @@
+== Localisation Update ==
+Localisation Update extension can update the MediaWiki messages at any time,
+without needing to upgrade the MediaWiki software.
+
+For more information see:
+ https://www.mediawiki.org/wiki/Extension:LocalisationUpdate
+
+== Installation ==
+1. Add the following to LocalSettings.php of your MediaWiki setup:
+
+ wfLoadExtension( 'LocalisationUpdate' );
+ $wgLocalisationUpdateDirectory = "$IP/cache";
+
+2. Create a cache folder in the installation directory, and be sure the server
+has permissions to write on it.
+
+If localization updates don't seem to come through, you may need to run,
+
+ php maintenance/rebuildLocalisationCache.php --force.
+
+3. Whenever you want to run an update, run,
+
+ php extensions/LocalisationUpdate/update.php
+
+For detailed help, see:
+
+ php extensions/LocalisationUpdate/update.php --help
+
+4. If you are on Unix like system, you should add LocalisationUpdate to
+crontab:
+
+ crontab -e
+ # Add the following line
+ @daily php /path/to/your/wiki/extensions/LocalisationUpdate/update.php --quiet
diff --git a/www/wiki/extensions/LocalisationUpdate/Updater.php b/www/wiki/extensions/LocalisationUpdate/Updater.php
new file mode 100644
index 00000000..7f0c21f7
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/Updater.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Executes the localisation update.
+ */
+class Updater {
+ /**
+ * Whether the path is a pattern and thus we need to use appropriate
+ * code for fetching directories.
+ *
+ * @param string $path Url
+ * @return bool
+ */
+ public function isDirectory( $path ) {
+ $filename = basename( $path );
+ return strpos( $filename, '*' ) !== false;
+ }
+
+ /**
+ * Expands repository relative path to full url with the given repository
+ * patterns. Extra variables in $info are used as variables and will be
+ * replaced the pattern.
+ *
+ * @param array $info Component information.
+ * @param array $repos Repository information.
+ * @return string
+ */
+ public function expandRemotePath( $info, $repos ) {
+ $pattern = $repos[$info['repo']];
+ unset( $info['repo'], $info['orig'] );
+
+ // This assumes all other keys are used as variables
+ // in the pattern. For example name -> %NAME%.
+ $keys = [];
+ foreach ( array_keys( $info ) as $key ) {
+ $keys[] = '%' . strtoupper( $key ) . '%';
+ }
+
+ $values = array_values( $info );
+ return str_replace( $keys, $values, $pattern );
+ }
+
+ /**
+ * Parses translations from given list of files.
+ *
+ * @param ReaderFactory $readerFactory Factory to construct parsers.
+ * @param array $files List of files with their contents as array values.
+ * @return array List of translations indexed by language code.
+ */
+ public function readMessages( ReaderFactory $readerFactory, array $files ) {
+ $messages = [];
+
+ foreach ( $files as $filename => $contents ) {
+ $reader = $readerFactory->getReader( $filename );
+ try {
+ $parsed = $reader->parse( $contents );
+ } catch ( \Exception $e ) {
+ trigger_error( __METHOD__ . ": Unable to parse messages from $filename", E_USER_WARNING );
+ continue;
+ }
+
+ foreach ( $parsed as $code => $langMessages ) {
+ if ( !isset( $messages[$code] ) ) {
+ $messages[$code] = [];
+ }
+ $messages[$code] = array_merge( $messages[$code], $langMessages );
+ }
+
+ $c = array_sum( array_map( 'count', $parsed ) );
+ // Useful for debugging, maybe create interface to pass this to the script?
+ # echo "$filename with " . get_class( $reader ) . " and $c\n";
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Find new and changed translations in $remote and returns them.
+ *
+ * @param array $origin
+ * @param array $remote
+ * @param array $blacklist Array of message keys to ignore, keys as as array keys.
+ * @return array
+ */
+ public function findChangedTranslations( $origin, $remote, $blacklist = [] ) {
+ $changed = [];
+ foreach ( $remote as $key => $value ) {
+ if ( isset( $blacklist[$key] ) ) {
+ continue;
+ }
+
+ if ( !isset( $origin[$key] ) || $value !== $origin[$key] ) {
+ $changed[$key] = $value;
+ }
+ }
+ return $changed;
+ }
+
+ /**
+ * Fetches files from given Url pattern.
+ *
+ * @param FetcherFactory $factory Factory to construct fetchers.
+ * @param string $path Url to the file or pattern of files.
+ * @return array List of Urls with file contents as path.
+ */
+ public function fetchFiles( FetcherFactory $factory, $path ) {
+ $fetcher = $factory->getFetcher( $path );
+
+ if ( $this->isDirectory( $path ) ) {
+ $files = $fetcher->fetchDirectory( $path );
+ } else {
+ $files = [ $path => $fetcher->fetchFile( $path ) ];
+ }
+
+ // Remove files which were not found
+ return array_filter( $files );
+ }
+
+ public function execute(
+ Finder $finder,
+ ReaderFactory $readerFactory,
+ FetcherFactory $fetcherFactory,
+ array $repos
+ ) {
+ $components = $finder->getComponents();
+
+ $updatedMessages = [];
+
+ foreach ( $components as $key => $info ) {
+ $originFiles = $this->fetchFiles( $fetcherFactory, $info['orig'] );
+ $remoteFiles = $this->fetchFiles( $fetcherFactory, $this->expandRemotePath( $info, $repos ) );
+
+ if ( $remoteFiles === [] ) {
+ // Small optimization: if nothing to compare with, skip
+ continue;
+ }
+
+ $originMessages = $this->readMessages( $readerFactory, $originFiles );
+ $remoteMessages = $this->readMessages( $readerFactory, $remoteFiles );
+
+ if ( !isset( $remoteMessages['en'] ) ) {
+ // Could not find remote messages
+ continue;
+ }
+
+ // If remote translation in English is not present or differs, we do not want
+ // translations for other languages for those messages, as they are either not
+ // used in this version of code or can be incompatible.
+ $forbiddenKeys = $this->findChangedTranslations(
+ $originMessages['en'],
+ $remoteMessages['en']
+ );
+
+ // We never accept updates for English strings
+ unset( $originMessages['en'], $remoteMessages['en'] );
+
+ // message: string in all languages; translation: string in one language.
+ foreach ( $remoteMessages as $language => $remoteTranslations ) {
+ // Check for completely new languages
+ $originTranslations = [];
+ if ( isset( $originMessages[$language] ) ) {
+ $originTranslations = $originMessages[$language];
+ }
+
+ $updatedTranslations = $this->findChangedTranslations(
+ $originTranslations,
+ $remoteTranslations,
+ $forbiddenKeys
+ );
+
+ // Avoid empty arrays
+ if ( $updatedTranslations === [] ) {
+ continue;
+ }
+
+ if ( !isset( $updatedMessages[$language] ) ) {
+ $updatedMessages[$language] = [];
+ }
+
+ // In case of conflicts, which should not exist, this prefers the
+ // first translation seen.
+ $updatedMessages[$language] += $updatedTranslations;
+ }
+ }
+
+ return $updatedMessages;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/composer.json b/www/wiki/extensions/LocalisationUpdate/composer.json
new file mode 100644
index 00000000..e49e334b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "mediawiki/localisation-update",
+ "type": "mediawiki-extension",
+ "description": "MediaWiki extension to keep the localised messages as up to date as possible.",
+ "keywords": [
+ "MediaWiki",
+ "l10n",
+ "localization"
+ ],
+ "homepage": "https://www.mediawiki.org/wiki/Extension:LocalisationUpdate",
+ "license": "GPL-2.0-or-later",
+ "authors": [
+ {
+ "name": "Roan Kattouw",
+ "email": "roan.kattouw@gmail.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Niklas Laxström",
+ "email": "niklas.laxstrom@gmail.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Tom Maaswinkel",
+ "role": "Original author"
+ }
+ ],
+ "support": {
+ "issues": "https://phabricator.wikimedia.org/",
+ "irc": "irc://irc.freenode.net/mediawiki",
+ "forum": "https://www.mediawiki.org/wiki/Extension_talk:LocalisationUpdate",
+ "wiki": "https://www.mediawiki.org/wiki/Extension:LocalisationUpdate"
+ },
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "1.0.0",
+ "mediawiki/mediawiki-codesniffer": "18.0.0",
+ "jakub-onderka/php-console-highlighter": "0.3.2",
+ "mediawiki/minus-x": "0.3.1",
+ "mediawiki/mediawiki-phan-config": "0.2.0"
+ },
+ "scripts": {
+ "fix": [
+ "phpcbf",
+ "minus-x fix ."
+ ],
+ "test": [
+ "parallel-lint . --exclude vendor --exclude node_modules",
+ "phpcs -p -s",
+ "minus-x check ."
+ ]
+ },
+ "extra": {
+ "phan-taint-check-plugin": "1.2.0"
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/extension.json b/www/wiki/extensions/LocalisationUpdate/extension.json
new file mode 100644
index 00000000..901f66fc
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/extension.json
@@ -0,0 +1,55 @@
+{
+ "@note": "This file must be kept in sync with LocalisationUpdate.php",
+ "name": "LocalisationUpdate",
+ "namemsg": "localisationupdate-extensionname",
+ "version": "1.4.0",
+ "author": [
+ "Tom Maaswinkel",
+ "Niklas Laxström",
+ "Roan Kattouw"
+ ],
+ "url": "https://www.mediawiki.org/wiki/Extension:LocalisationUpdate",
+ "descriptionmsg": "localisationupdate-desc",
+ "license-name": "GPL-2.0-or-later",
+ "type": "other",
+ "MessagesDirs": {
+ "LocalisationUpdate": [
+ "i18n"
+ ]
+ },
+ "AutoloadClasses": {
+ "LocalisationUpdate": "includes/LocalisationUpdate.php",
+ "LocalisationUpdate\\Updater": "includes/Updater.php",
+ "QuickArrayReader": "includes/QuickArrayReader.php",
+ "LocalisationUpdate\\Fetcher": "includes/fetcher/Fetcher.php",
+ "LocalisationUpdate\\FetcherFactory": "includes/fetcher/FetcherFactory.php",
+ "LocalisationUpdate\\FileSystemFetcher": "includes/fetcher/FileSystemFetcher.php",
+ "LocalisationUpdate\\GitHubFetcher": "includes/fetcher/GitHubFetcher.php",
+ "LocalisationUpdate\\HttpFetcher": "includes/fetcher/HttpFetcher.php",
+ "LocalisationUpdate\\Finder": "includes/finder/Finder.php",
+ "LocalisationUpdate\\JSONReader": "includes/reader/JSONReader.php",
+ "LocalisationUpdate\\PHPReader": "includes/reader/PHPReader.php",
+ "LocalisationUpdate\\Reader": "includes/reader/Reader.php",
+ "LocalisationUpdate\\ReaderFactory": "includes/reader/ReaderFactory.php"
+ },
+ "Hooks": {
+ "LocalisationCacheRecache": [
+ "LocalisationUpdate::onRecache"
+ ],
+ "LocalisationCacheRecacheFallback": [
+ "LocalisationUpdate::onRecacheFallback"
+ ]
+ },
+ "config": {
+ "LocalisationUpdateDirectory": false,
+ "LocalisationUpdateRepository": "github",
+ "LocalisationUpdateRepositories": {
+ "github": {
+ "mediawiki": "https://raw.github.com/wikimedia/mediawiki/master/%PATH%",
+ "extension": "https://raw.github.com/wikimedia/mediawiki-extensions-%NAME%/master/%PATH%",
+ "skin": "https://raw.github.com/wikimedia/mediawiki-skins-%NAME%/master/%PATH%"
+ }
+ }
+ },
+ "manifest_version": 1
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/fetcher/Fetcher.php b/www/wiki/extensions/LocalisationUpdate/fetcher/Fetcher.php
new file mode 100644
index 00000000..9ff79584
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/fetcher/Fetcher.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for classes which fetch files over different protocols and ways.
+ */
+interface Fetcher {
+ /**
+ * Fetches a single resource.
+ *
+ * @return bool|string False on failure.
+ */
+ public function fetchFile( $url );
+
+ /**
+ * Fetch a list of resources. This has the benefit of being able to pick up
+ * new languages as they appear if languages are stored in separate files.
+ *
+ * @return array
+ */
+ public function fetchDirectory( $pattern );
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/fetcher/FetcherFactory.php b/www/wiki/extensions/LocalisationUpdate/fetcher/FetcherFactory.php
new file mode 100644
index 00000000..570dc7e0
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/fetcher/FetcherFactory.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Constructs fetchers based on the repository urls.
+ */
+class FetcherFactory {
+ public function getFetcher( $path ) {
+ if ( strpos( $path, 'https://raw.github.com/' ) === 0 ) {
+ return new GitHubFetcher();
+ } elseif ( strpos( $path, 'http://' ) === 0 ) {
+ return new HttpFetcher();
+ } elseif ( strpos( $path, 'https://' ) === 0 ) {
+ return new HttpFetcher();
+ } else {
+ return new FileSystemFetcher();
+ }
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/fetcher/FileSystemFetcher.php b/www/wiki/extensions/LocalisationUpdate/fetcher/FileSystemFetcher.php
new file mode 100644
index 00000000..6d215867
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/fetcher/FileSystemFetcher.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Accesses file system directly.
+ */
+class FileSystemFetcher implements Fetcher {
+ public function fetchFile( $url ) {
+ // Remove the protocol prefix
+ $url = preg_replace( '~^file://~', '', $url );
+
+ if ( !is_readable( $url ) ) {
+ return false;
+ }
+
+ return file_get_contents( $url );
+ }
+
+ public function fetchDirectory( $pattern ) {
+ // Remove the protocol prefix
+ $pattern = preg_replace( '~^file://~', '', $pattern );
+
+ $data = [];
+ foreach ( glob( $pattern ) as $file ) {
+ if ( is_readable( $file ) ) {
+ $data["file://$file"] = file_get_contents( $file );
+ }
+ }
+ return $data;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/fetcher/GitHubFetcher.php b/www/wiki/extensions/LocalisationUpdate/fetcher/GitHubFetcher.php
new file mode 100644
index 00000000..5bc7d4f2
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/fetcher/GitHubFetcher.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * This class uses GitHub api to obtain a list of files present in a directory
+ * to avoid fetching files that don't exist.
+ *
+ * @todo Could use file hashes to 1) avoid fetching files with same hash as
+ * the source. 2) avoid fetching files which haven't changed since last check
+ * if we store them.
+ */
+class GitHubFetcher extends HttpFetcher {
+ public function fetchDirectory( $pattern ) {
+ $domain = preg_quote( 'https://raw.github.com/', '~' );
+ $p = "~^$domain(?P<org>[^/]+)/(?P<repo>[^/]+)/(?P<branch>[^/]+)/(?P<path>.+)/.+$~";
+ preg_match( $p, $pattern, $m );
+
+ $apiURL = "https://api.github.com/repos/{$m['org']}/{$m['repo']}/contents/{$m['path']}";
+ $json = \Http::get( $apiURL );
+ if ( !$json ) {
+ throw new \Exception( "Unable to get directory listing for {$m['org']}/{$m['repo']}" );
+ }
+
+ $files = [];
+ $json = \FormatJson::decode( $json, true );
+ foreach ( $json as $fileinfo ) {
+ $fileurl = dirname( $pattern ) . '/' . $fileinfo['name'];
+ $file = $this->fetchFile( $fileurl );
+ if ( $file ) {
+ $files[$fileurl] = $file;
+ }
+ }
+ return $files;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/fetcher/HttpFetcher.php b/www/wiki/extensions/LocalisationUpdate/fetcher/HttpFetcher.php
new file mode 100644
index 00000000..42b0f032
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/fetcher/HttpFetcher.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Fetches files over HTTP(s).
+ */
+class HttpFetcher implements Fetcher {
+ public function fetchFile( $url ) {
+ return \Http::get( $url );
+ }
+
+ /**
+ * This is horribly inefficient. Subclasses have more efficient
+ * implementation of this.
+ */
+ public function fetchDirectory( $pattern ) {
+ $files = [];
+
+ $languages = \Language::fetchLanguageNames( null, 'mwfile' );
+
+ foreach ( array_keys( $languages ) as $code ) {
+ // Hack for core
+ if ( strpos( $pattern, 'Messages*.php' ) !== false ) {
+ $code = ucfirst( strtr( $code, '-', '_' ) );
+ }
+
+ $url = str_replace( '*', $code, $pattern );
+ $file = $this->fetchFile( $url );
+ if ( $file ) {
+ $files[$url] = $file;
+ }
+ }
+
+ return $files;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/finder/Finder.php b/www/wiki/extensions/LocalisationUpdate/finder/Finder.php
new file mode 100644
index 00000000..20ba9377
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/finder/Finder.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for classes which provide list of components, which should be
+ * included for l10n updates.
+ */
+class Finder {
+ /**
+ * @param array $php See $wgExtensionMessagesFiles
+ * @param array $json See $wgMessagesDirs
+ * @param string $core Absolute path to MediaWiki core
+ */
+ public function __construct( $php, $json, $core ) {
+ $this->php = $php;
+ $this->json = $json;
+ $this->core = $core;
+ }
+
+ /**
+ * @return array
+ */
+ public function getComponents() {
+ $components = [];
+
+ // For older versions of Mediawiki, pull json updates even though its still using php
+ if ( !isset( $this->json['core'] ) ) {
+ $components['core'] = [
+ 'repo' => 'mediawiki',
+ 'orig' => "file://{$this->core}/languages/messages/Messages*.php",
+ 'path' => 'languages/messages/i18n/*.json',
+ ];
+ }
+
+ foreach ( $this->json as $key => $value ) {
+ // Json should take priority if both exist
+ unset( $this->php[$key] );
+
+ foreach ( (array)$value as $subkey => $subvalue ) {
+ // Mediawiki core files
+ $matches = [];
+ if ( preg_match( '~/(?P<path>(?:includes|languages|resources)/.*)$~', $subvalue, $matches ) ) {
+ $components["$key-$subkey"] = [
+ 'repo' => 'mediawiki',
+ 'orig' => "file://$value/*.json",
+ 'path' => "{$matches['path']}/*.json",
+ ];
+ continue;
+ }
+
+ $item = $this->getItem( 'extensions', $subvalue );
+ if ( $item !== null ) {
+ $item['repo'] = 'extension';
+ $components["$key-$subkey"] = $item;
+ continue;
+ }
+
+ $item = $this->getItem( 'skins', $subvalue );
+ if ( $item !== null ) {
+ $item['repo'] = 'skin';
+ $components["$key-$subkey"] = $item;
+ continue;
+ }
+ }
+ }
+
+ foreach ( $this->php as $key => $value ) {
+ $matches = [];
+ $ok = preg_match( '~/extensions/(?P<name>[^/]+)/(?P<path>.*\.i18n\.php)$~', $value, $matches );
+ if ( !$ok ) {
+ continue;
+ }
+
+ $components[$key] = [
+ 'repo' => 'extension',
+ 'name' => $matches['name'],
+ 'orig' => "file://$value",
+ 'path' => $matches['path'],
+ ];
+ }
+
+ return $components;
+ }
+
+ /**
+ * @param string $dir extensions or skins
+ * @param string $subvalue
+ * @return array|null
+ */
+ private function getItem( $dir, $subvalue ) {
+ // This ignores magic, alias etc. non message files
+ $matches = [];
+ if ( !preg_match( "~/$dir/(?P<name>[^/]+)/(?P<path>.*)$~", $subvalue, $matches ) ) {
+ return null;
+ }
+
+ return [
+ 'name' => $matches['name'],
+ 'orig' => "file://$subvalue/*.json",
+ 'path' => "{$matches['path']}/*.json",
+ ];
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/af.json b/www/wiki/extensions/LocalisationUpdate/i18n/af.json
new file mode 100644
index 00000000..3739dc26
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/af.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Naudefj"
+ ]
+ },
+ "localisationupdate-desc": "Hou die gelokaliseerde boodskappe so op datum as moontlik"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ar.json b/www/wiki/extensions/LocalisationUpdate/i18n/ar.json
new file mode 100644
index 00000000..101e54ee
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ar.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Meno25"
+ ]
+ },
+ "localisationupdate-desc": "يبقي الرسائل المترجمة محدثة كأفضل ما يكون"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ast.json b/www/wiki/extensions/LocalisationUpdate/i18n/ast.json
new file mode 100644
index 00000000..13fcd456
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ast.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Xuacu"
+ ]
+ },
+ "localisationupdate-desc": "Caltién los mensaxes llocalizaos tan anovaos como se pueda"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ba.json b/www/wiki/extensions/LocalisationUpdate/i18n/ba.json
new file mode 100644
index 00000000..e96ee2b4
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ba.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Assele"
+ ]
+ },
+ "localisationupdate-desc": "Локалләштерелгән хәбәрҙәрҙең мөмкин тиклем яңы булыуын тәьмин итә"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/bar.json b/www/wiki/extensions/LocalisationUpdate/i18n/bar.json
new file mode 100644
index 00000000..0c4c56f7
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/bar.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Man77"
+ ]
+ },
+ "localisationupdate-desc": "Lokalisiade Texte und Nåchrichtn so aktuell håidn wia's gråd gehd"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/be-tarask.json b/www/wiki/extensions/LocalisationUpdate/i18n/be-tarask.json
new file mode 100644
index 00000000..01f487be
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/be-tarask.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "EugeneZelenko",
+ "Wizardist"
+ ]
+ },
+ "localisationupdate-desc": "Сочыць за актуальнасьцю лякалізаваных паведамленьняў, наколькі гэта магчыма"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/bg.json b/www/wiki/extensions/LocalisationUpdate/i18n/bg.json
new file mode 100644
index 00000000..59a6edbd
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/bg.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "DCLXVI"
+ ]
+ },
+ "localisationupdate-desc": "Поддържа локализираните съобщения възможно най-актуални"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/bn.json b/www/wiki/extensions/LocalisationUpdate/i18n/bn.json
new file mode 100644
index 00000000..6936be62
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/bn.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bellayet"
+ ]
+ },
+ "localisationupdate-desc": "স্থানীয়করণকৃত বার্তাসমূহ যথাসম্ভব হালনাগাদ রাখে"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/br.json b/www/wiki/extensions/LocalisationUpdate/i18n/br.json
new file mode 100644
index 00000000..89db4778
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/br.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fulup"
+ ]
+ },
+ "localisationupdate-desc": "Derc'hel da hizivaat ar c'hemennoù troet ken fonnus ha ma'z eus tu"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/bs.json b/www/wiki/extensions/LocalisationUpdate/i18n/bs.json
new file mode 100644
index 00000000..339dae1d
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/bs.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "CERminator"
+ ]
+ },
+ "localisationupdate-desc": "Zadržavanje lokaliziranih poruka ažurnim koliko je god moguće"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ca.json b/www/wiki/extensions/LocalisationUpdate/i18n/ca.json
new file mode 100644
index 00000000..4b4ac0af
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ca.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Paucabot",
+ "Fitoschido"
+ ]
+ },
+ "localisationupdate-desc": "Manté els missatges traduïts tan actualitzats com sigui possible"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ce.json b/www/wiki/extensions/LocalisationUpdate/i18n/ce.json
new file mode 100644
index 00000000..579d7dbb
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ce.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Умар"
+ ]
+ },
+ "localisationupdate-desc": "Таро ма хуьйла хаамашан болх бан гӀо до"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/cs.json b/www/wiki/extensions/LocalisationUpdate/i18n/cs.json
new file mode 100644
index 00000000..d72e9110
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/cs.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mormegil"
+ ]
+ },
+ "localisationupdate-desc": "Udržuje lokalizovaná hlášení co možná nejaktuálnější"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/cy.json b/www/wiki/extensions/LocalisationUpdate/i18n/cy.json
new file mode 100644
index 00000000..9ab89a53
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/cy.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lloffiwr"
+ ]
+ },
+ "localisationupdate-desc": "Yn diweddaru'r cyfieithiadau o negeseuon mor aml â phosib"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/da.json b/www/wiki/extensions/LocalisationUpdate/i18n/da.json
new file mode 100644
index 00000000..645d0842
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/da.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Peter Alberti"
+ ]
+ },
+ "localisationupdate-desc": "Holder de lokaliserede meddelelser så opdaterede som muligt"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/de.json b/www/wiki/extensions/LocalisationUpdate/i18n/de.json
new file mode 100644
index 00000000..98f9dc14
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/de.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kghbln",
+ "Purodha",
+ "Metalhead64"
+ ]
+ },
+ "localisationupdate-extensionname": "LocalisationUpdate",
+ "localisationupdate-desc": "Ermöglicht es lokalisierte Texte und Nachrichten so aktuell wie möglich zu halten"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/dsb.json b/www/wiki/extensions/LocalisationUpdate/i18n/dsb.json
new file mode 100644
index 00000000..11cbf5d9
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/dsb.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michawiki"
+ ]
+ },
+ "localisationupdate-desc": "Źaržy lokalizěrowane powěźeńki tak aktualne ako móžno"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/el.json b/www/wiki/extensions/LocalisationUpdate/i18n/el.json
new file mode 100644
index 00000000..7b4db44c
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/el.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Omnipaedista"
+ ]
+ },
+ "localisationupdate-desc": "Διατηρεί τις μεταφράσεις μηνυμάτων όσο πιο ενημερωμένες γίνεται"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/en.json b/www/wiki/extensions/LocalisationUpdate/i18n/en.json
new file mode 100644
index 00000000..a15cef56
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/en.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tom Maaswinkel"
+ ]
+ },
+ "localisationupdate-extensionname": "LocalisationUpdate",
+ "localisationupdate-desc": "Keeps the localised messages as up to date as possible"
+} \ No newline at end of file
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/eo.json b/www/wiki/extensions/LocalisationUpdate/i18n/eo.json
new file mode 100644
index 00000000..c1251faa
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/eo.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Yekrats"
+ ]
+ },
+ "localisationupdate-desc": "Ĝisdatigas la asimilitajn mesaĝojn tiom eble"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/es.json b/www/wiki/extensions/LocalisationUpdate/i18n/es.json
new file mode 100644
index 00000000..aea82b47
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/es.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Crazymadlover",
+ "Fitoschido"
+ ]
+ },
+ "localisationupdate-desc": "Mantiene los mensajes traducidos tan actualizados como sea posible"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/et.json b/www/wiki/extensions/LocalisationUpdate/i18n/et.json
new file mode 100644
index 00000000..9cf22620
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/et.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Pikne"
+ ]
+ },
+ "localisationupdate-desc": "Hoiab lokaliseeritud sõnumid nii ajakohased kui võimalik."
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/eu.json b/www/wiki/extensions/LocalisationUpdate/i18n/eu.json
new file mode 100644
index 00000000..3f55906c
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/eu.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kobazulo"
+ ]
+ },
+ "localisationupdate-desc": "Itzulitako mezuak ahalik eta eguneratuen mantentzen ditu"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/fa.json b/www/wiki/extensions/LocalisationUpdate/i18n/fa.json
new file mode 100644
index 00000000..ae1a2a27
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/fa.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "ZxxZxxZ"
+ ]
+ },
+ "localisationupdate-desc": "پیغام‌های محلی‌سازی‌شده را تا جای ممکن به‌روز نگه می‌دارد"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/fi.json b/www/wiki/extensions/LocalisationUpdate/i18n/fi.json
new file mode 100644
index 00000000..253c2de7
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/fi.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Crt",
+ "Nike"
+ ]
+ },
+ "localisationupdate-desc": "Pitää ohjelmiston käännöksen ajantasaisena."
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/fr.json b/www/wiki/extensions/LocalisationUpdate/i18n/fr.json
new file mode 100644
index 00000000..b62e2b6b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/fr.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Crochet.david"
+ ]
+ },
+ "localisationupdate-desc": "Maintenir la traduction des messages à jour autant que possible"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/gl.json b/www/wiki/extensions/LocalisationUpdate/i18n/gl.json
new file mode 100644
index 00000000..f80215b3
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/gl.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Toliño"
+ ]
+ },
+ "localisationupdate-desc": "Mantén as mensaxes localizadas tan actualizadas como é posible"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/gsw.json b/www/wiki/extensions/LocalisationUpdate/i18n/gsw.json
new file mode 100644
index 00000000..9ce091c0
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/gsw.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Als-Holder"
+ ]
+ },
+ "localisationupdate-desc": "Halt d Syschtemnochrichte so aktuälle wie megli"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/gu.json b/www/wiki/extensions/LocalisationUpdate/i18n/gu.json
new file mode 100644
index 00000000..d0fb3fdb
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/gu.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "KartikMistry"
+ ]
+ },
+ "localisationupdate-desc": "ભાષાંતરોને શક્ય હોય ત્યાં સુધી છેલ્લામાં છેલ્લાં રાખે છે"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/he.json b/www/wiki/extensions/LocalisationUpdate/i18n/he.json
new file mode 100644
index 00000000..9a1b8c64
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/he.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "YaronSh"
+ ]
+ },
+ "localisationupdate-desc": "שמירת ההודעות המתורגמות מעודכנות ככל הניתן"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/hil.json b/www/wiki/extensions/LocalisationUpdate/i18n/hil.json
new file mode 100644
index 00000000..cff1ac40
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/hil.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tagimata"
+ ]
+ },
+ "localisationupdate-desc": "Gatugo sang mga mensahe nga lokal para mapahibalo sang madali"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/hr.json b/www/wiki/extensions/LocalisationUpdate/i18n/hr.json
new file mode 100644
index 00000000..8d196c34
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/hr.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "SpeedyGonsales"
+ ]
+ },
+ "localisationupdate-desc": "Dogradnja za osvježavanje lokalizacije poruka MediaWikija"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/hsb.json b/www/wiki/extensions/LocalisationUpdate/i18n/hsb.json
new file mode 100644
index 00000000..04adee04
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/hsb.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michawiki"
+ ]
+ },
+ "localisationupdate-desc": "Dźerži lokalizowane zdźělenki tak aktualne kaž móžno"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/hu.json b/www/wiki/extensions/LocalisationUpdate/i18n/hu.json
new file mode 100644
index 00000000..f8bd039b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/hu.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Glanthor Reviol"
+ ]
+ },
+ "localisationupdate-desc": "Frissíti a lefordított üzeneteket"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ia.json b/www/wiki/extensions/LocalisationUpdate/i18n/ia.json
new file mode 100644
index 00000000..7ab58ab9
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ia.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "McDutchie"
+ ]
+ },
+ "localisationupdate-desc": "Mantene le messages localisate tanto actual como possibile"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/id.json b/www/wiki/extensions/LocalisationUpdate/i18n/id.json
new file mode 100644
index 00000000..6f46173a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/id.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bennylin"
+ ]
+ },
+ "localisationupdate-desc": "Mengusahakan agar pesan-pesan yang telah diterjemahkan tetap semutakhir mungkin"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ilo.json b/www/wiki/extensions/LocalisationUpdate/i18n/ilo.json
new file mode 100644
index 00000000..56428785
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ilo.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lam-ang"
+ ]
+ },
+ "localisationupdate-desc": "Taginayonenna a mapabaro dagiti naipatarus a mensahe"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/it.json b/www/wiki/extensions/LocalisationUpdate/i18n/it.json
new file mode 100644
index 00000000..b1ee5aa3
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/it.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Darth Kule"
+ ]
+ },
+ "localisationupdate-desc": "Mantiene i messaggi localizzati quanto più aggiornati è possibile"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ja.json b/www/wiki/extensions/LocalisationUpdate/i18n/ja.json
new file mode 100644
index 00000000..8eba705e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ja.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fryed-peach",
+ "Shirayuki"
+ ]
+ },
+ "localisationupdate-desc": "メッセージの翻訳をできるだけ最新に保つ"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/km.json b/www/wiki/extensions/LocalisationUpdate/i18n/km.json
new file mode 100644
index 00000000..c3f78d86
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/km.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "វ័ណថារិទ្ធ"
+ ]
+ },
+ "localisationupdate-desc": "រក្សា​សារ​ដែលបាន​ប្រែសម្រួល​ទាំងឡាយ អោយនៅ​ទាន់សម័យ​តាមដែលអាចធ្វើទៅបាន​"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ko.json b/www/wiki/extensions/LocalisationUpdate/i18n/ko.json
new file mode 100644
index 00000000..0f906a59
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ko.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kwj2772",
+ "아라"
+ ]
+ },
+ "localisationupdate-desc": "번역된 메시지를 가능한 한 최신으로 유지합니다"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ksh.json b/www/wiki/extensions/LocalisationUpdate/i18n/ksh.json
new file mode 100644
index 00000000..ecb69cd7
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ksh.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Purodha"
+ ]
+ },
+ "localisationupdate-desc": "Texte un Nohreeschte vum Wiki esu joot wi müjjelich om neueste Shtand halde"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/lb.json b/www/wiki/extensions/LocalisationUpdate/i18n/lb.json
new file mode 100644
index 00000000..f2eed733
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/lb.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Robby",
+ "Soued031"
+ ]
+ },
+ "localisationupdate-desc": "hält déi lokaliséiert Messagen sou aktuell wéi méiglech."
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/lij.json b/www/wiki/extensions/LocalisationUpdate/i18n/lij.json
new file mode 100644
index 00000000..8e699bbc
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/lij.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Giromin Cangiaxo"
+ ]
+ },
+ "localisationupdate-desc": "O manten i messaggi localizæ ciù agiornæ che se poeu"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/mk.json b/www/wiki/extensions/LocalisationUpdate/i18n/mk.json
new file mode 100644
index 00000000..3ac7e4df
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/mk.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bjankuloski06"
+ ]
+ },
+ "localisationupdate-extensionname": "Поднова на локализацијата",
+ "localisationupdate-desc": "Ги одржува локализираните пораки колку што е можно пообновени и повеќе во тек со настаните"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ml.json b/www/wiki/extensions/LocalisationUpdate/i18n/ml.json
new file mode 100644
index 00000000..5e7dfd76
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ml.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Praveenp"
+ ]
+ },
+ "localisationupdate-desc": "പ്രാദേശികഭാഷയിലാക്കിയ സന്ദേശങ്ങൾ കഴിയുന്നത്ര വേഗം ചേർക്കാൻ ഉപയോഗിക്കുന്നു"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ms.json b/www/wiki/extensions/LocalisationUpdate/i18n/ms.json
new file mode 100644
index 00000000..5081ed8e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ms.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Anakmalaysia"
+ ]
+ },
+ "localisationupdate-desc": "Memastikan kekemaskinian mesej-mesej yang disetempatkan"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/nb.json b/www/wiki/extensions/LocalisationUpdate/i18n/nb.json
new file mode 100644
index 00000000..e9db5be1
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/nb.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Nghtwlkr",
+ "Jon Harald Søby"
+ ]
+ },
+ "localisationupdate-extensionname": "LocalisationUpdate",
+ "localisationupdate-desc": "Holder de lokaliserte meldingene så oppdaterte som mulig"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/nl.json b/www/wiki/extensions/LocalisationUpdate/i18n/nl.json
new file mode 100644
index 00000000..a05698e6
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/nl.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Siebrand"
+ ]
+ },
+ "localisationupdate-desc": "Houdt de gelokaliseerde berichten zo actueel mogelijk"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/nn.json b/www/wiki/extensions/LocalisationUpdate/i18n/nn.json
new file mode 100644
index 00000000..b9730f27
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/nn.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Gunnernett"
+ ]
+ },
+ "localisationupdate-desc": "Held dei lokaliserte meldingane så oppdaterte som mogleg"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/oc.json b/www/wiki/extensions/LocalisationUpdate/i18n/oc.json
new file mode 100644
index 00000000..541dd0b9
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/oc.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Cedric31"
+ ]
+ },
+ "localisationupdate-desc": "Manténer la traduccion dels messatges a jorn autant que possible"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/pl.json b/www/wiki/extensions/LocalisationUpdate/i18n/pl.json
new file mode 100644
index 00000000..2ed878ea
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/pl.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Sp5uhe"
+ ]
+ },
+ "localisationupdate-desc": "Uaktualnia lokalne komunikaty w miarę możliwości na bieżąco"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/pms.json b/www/wiki/extensions/LocalisationUpdate/i18n/pms.json
new file mode 100644
index 00000000..bebbbd8a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/pms.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Dragonòt"
+ ]
+ },
+ "localisationupdate-desc": "A manten i messagi localisà ël pì agiornà possìbil"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/pt-br.json b/www/wiki/extensions/LocalisationUpdate/i18n/pt-br.json
new file mode 100644
index 00000000..2684cfa9
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/pt-br.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Eduardo.mps"
+ ]
+ },
+ "localisationupdate-desc": "Mantém as mensagens localizadas tão atualizadas quanto possível"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/pt.json b/www/wiki/extensions/LocalisationUpdate/i18n/pt.json
new file mode 100644
index 00000000..bbda7981
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/pt.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Hamilton Abreu",
+ "Luckas",
+ "Malafaya"
+ ]
+ },
+ "localisationupdate-desc": "Mantém as mensagens traduzidas tão atualizadas quanto possível"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/qqq.json b/www/wiki/extensions/LocalisationUpdate/i18n/qqq.json
new file mode 100644
index 00000000..99da1a04
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/qqq.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fryed-peach",
+ "Purodha",
+ "Shirayuki",
+ "Umherirrender"
+ ]
+ },
+ "localisationupdate-extensionname": "{{name}}",
+ "localisationupdate-desc": "{{desc|name=Localisation Update|url=https://www.mediawiki.org/wiki/Extension:LocalisationUpdate}}"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ro.json b/www/wiki/extensions/LocalisationUpdate/i18n/ro.json
new file mode 100644
index 00000000..b609969c
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ro.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "KlaudiuMihaila"
+ ]
+ },
+ "localisationupdate-desc": "Menține mesajele localizate cât mai actualizate"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/roa-tara.json b/www/wiki/extensions/LocalisationUpdate/i18n/roa-tara.json
new file mode 100644
index 00000000..47d5c72c
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/roa-tara.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joetaras"
+ ]
+ },
+ "localisationupdate-desc": "Mandine le messagge localizzate 'u cchiù aggiornate possibbile"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ru.json b/www/wiki/extensions/LocalisationUpdate/i18n/ru.json
new file mode 100644
index 00000000..a0e4739e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ru.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Александр Сигачёв"
+ ]
+ },
+ "localisationupdate-desc": "Поддерживает актуальность локализованных сообщений, насколько это возможно"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/sk.json b/www/wiki/extensions/LocalisationUpdate/i18n/sk.json
new file mode 100644
index 00000000..396998ad
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/sk.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Helix84"
+ ]
+ },
+ "localisationupdate-desc": "Udržiava lokalizované správy čo najaktuálnejšie"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/sr-ec.json b/www/wiki/extensions/LocalisationUpdate/i18n/sr-ec.json
new file mode 100644
index 00000000..dfd900a4
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/sr-ec.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Михајло Анђелковић"
+ ]
+ },
+ "localisationupdate-desc": "Ажурира локализоване поруке колико је то могуће"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/sr-el.json b/www/wiki/extensions/LocalisationUpdate/i18n/sr-el.json
new file mode 100644
index 00000000..f303273b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/sr-el.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Liangent"
+ ]
+ },
+ "localisationupdate-desc": "Ažurira lokalizovane poruke koliko je to moguće"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/su.json b/www/wiki/extensions/LocalisationUpdate/i18n/su.json
new file mode 100644
index 00000000..2c9d2f13
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/su.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kandar"
+ ]
+ },
+ "localisationupdate-desc": "Ngajaga sangkan talatah-talatah nu geus dialihbasakeun salawasnya mutahir"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/sv.json b/www/wiki/extensions/LocalisationUpdate/i18n/sv.json
new file mode 100644
index 00000000..a68f4521
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/sv.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Boivie"
+ ]
+ },
+ "localisationupdate-desc": "Håller de lokaliserade meddelandena så uppdaterade som möjligt"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/ta.json b/www/wiki/extensions/LocalisationUpdate/i18n/ta.json
new file mode 100644
index 00000000..153519e6
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/ta.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "செல்வா"
+ ]
+ },
+ "localisationupdate-desc": "உட்சூழலுக்கான செய்திகளை கூடியமட்டிலும் இன்றையநிலையில் வைக்கப்பட்டுள்ளன"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/te.json b/www/wiki/extensions/LocalisationUpdate/i18n/te.json
new file mode 100644
index 00000000..4bc8cd36
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/te.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Veeven"
+ ]
+ },
+ "localisationupdate-desc": "స్ధానికీకరించిన సందేశాలను సాధ్యమైనంత తాజాగా ఉంచుతుంది"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/tl.json b/www/wiki/extensions/LocalisationUpdate/i18n/tl.json
new file mode 100644
index 00000000..e493bc7b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/tl.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "AnakngAraw"
+ ]
+ },
+ "localisationupdate-desc": "Pinananatili ang mga mensaheng lokalisado bilang pinaka nasasapanahon"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/tr.json b/www/wiki/extensions/LocalisationUpdate/i18n/tr.json
new file mode 100644
index 00000000..63191719
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/tr.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joseph"
+ ]
+ },
+ "localisationupdate-desc": "Yerelleştirilen mesajları mümkün olabildiğince güncel tutar"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/uk.json b/www/wiki/extensions/LocalisationUpdate/i18n/uk.json
new file mode 100644
index 00000000..e2c2446e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/uk.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Prima klasy4na"
+ ]
+ },
+ "localisationupdate-desc": "Забезпечує оновлення локалізованих повідомлень у міру можливості"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/vep.json b/www/wiki/extensions/LocalisationUpdate/i18n/vep.json
new file mode 100644
index 00000000..8f3916d5
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/vep.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Игорь Бродский"
+ ]
+ },
+ "localisationupdate-desc": "Pidab lokaliziruidud tedotused veresin, ku voib"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/vi.json b/www/wiki/extensions/LocalisationUpdate/i18n/vi.json
new file mode 100644
index 00000000..9c0e7091
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/vi.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Vinhtantran"
+ ]
+ },
+ "localisationupdate-desc": "Giữ các thông điệp bản địa hóa được cập nhật nhất có thể"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/wa.json b/www/wiki/extensions/LocalisationUpdate/i18n/wa.json
new file mode 100644
index 00000000..cba6cba1
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/wa.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Srtxg"
+ ]
+ },
+ "localisationupdate-desc": "Po wårder les ratournaedjes di l' eterface li pus a djoû possibe"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/yi.json b/www/wiki/extensions/LocalisationUpdate/i18n/yi.json
new file mode 100644
index 00000000..eab1afc4
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/yi.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "פוילישער"
+ ]
+ },
+ "localisationupdate-desc": "האלטן די לאקאליזירטע מעלדונגען אקטועל ווי נאר מעגלעך"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/yue.json b/www/wiki/extensions/LocalisationUpdate/i18n/yue.json
new file mode 100644
index 00000000..1376dc0e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/yue.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tom Maaswinkel"
+ ]
+ },
+ "localisationupdate-desc": "將本地化嘅信息保持最新"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/zh-hans.json b/www/wiki/extensions/LocalisationUpdate/i18n/zh-hans.json
new file mode 100644
index 00000000..8141f127
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/zh-hans.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tom Maaswinkel"
+ ]
+ },
+ "localisationupdate-desc": "将本地化的信息保持最新"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/i18n/zh-hant.json b/www/wiki/extensions/LocalisationUpdate/i18n/zh-hant.json
new file mode 100644
index 00000000..511b9790
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/i18n/zh-hant.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mark85296341",
+ "Tom Maaswinkel",
+ "LNDDYL"
+ ]
+ },
+ "localisationupdate-desc": "將在地化的資訊盡可能保持最新"
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/LocalisationUpdate.php b/www/wiki/extensions/LocalisationUpdate/includes/LocalisationUpdate.php
new file mode 100644
index 00000000..a0b5f044
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/LocalisationUpdate.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Class for localization update hooks and static methods.
+ */
+class LocalisationUpdate {
+ /**
+ * Hook: LocalisationCacheRecacheFallback
+ * @param LocalisationCache $lc
+ * @param string $code
+ * @param array &$cache
+ * @return true
+ */
+ public static function onRecacheFallback( LocalisationCache $lc, $code, array &$cache ) {
+ $dir = self::getDirectory();
+ if ( !$dir ) {
+ return true;
+ }
+
+ $fileName = "$dir/" . self::getFilename( $code );
+ if ( is_readable( $fileName ) ) {
+ $data = FormatJson::decode( file_get_contents( $fileName ), true );
+ $cache['messages'] = array_merge( $cache['messages'], $data );
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook: LocalisationCacheRecache
+ * @param LocalisationCache $lc
+ * @param string $code
+ * @param array &$cache
+ * @return true
+ */
+ public static function onRecache( LocalisationCache $lc, $code, array &$cache ) {
+ $dir = self::getDirectory();
+ if ( !$dir ) {
+ return true;
+ }
+
+ $codeSequence = array_merge( [ $code ], $cache['fallbackSequence'] );
+ foreach ( $codeSequence as $csCode ) {
+ $fileName = "$dir/" . self::getFilename( $csCode );
+ $cache['deps'][] = new FileDependency( $fileName );
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a directory where updated translations are stored.
+ *
+ * @return string|false False if not configured.
+ * @since 1.1
+ */
+ public static function getDirectory() {
+ global $wgLocalisationUpdateDirectory, $wgCacheDirectory;
+
+ return $wgLocalisationUpdateDirectory ?: $wgCacheDirectory;
+ }
+
+ /**
+ * Returns a filename where updated translations are stored.
+ *
+ * @param string $language Language tag
+ * @return string
+ * @since 1.1
+ */
+ public static function getFilename( $language ) {
+ return "l10nupdate-$language.json";
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/QuickArrayReader.php b/www/wiki/extensions/LocalisationUpdate/includes/QuickArrayReader.php
new file mode 100644
index 00000000..0314ee68
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/QuickArrayReader.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * Quickie parser class that can happily read the subset of PHP we need
+ * for our localization arrays safely.
+ *
+ * Still an order of magnitude slower than eval().
+ */
+class QuickArrayReader {
+ private $vars = [];
+
+ /**
+ * @param string $string
+ */
+ function __construct( $string ) {
+ $scalarTypes = [
+ T_LNUMBER => true,
+ T_DNUMBER => true,
+ T_STRING => true,
+ T_CONSTANT_ENCAPSED_STRING => true,
+ ];
+ $skipTypes = [
+ T_WHITESPACE => true,
+ T_COMMENT => true,
+ T_DOC_COMMENT => true,
+ ];
+ $tokens = token_get_all( $string );
+ $count = count( $tokens );
+ for ( $i = 0; $i < $count; ) {
+ while ( isset( $skipTypes[$tokens[$i][0]] ) ) {
+ $i++;
+ }
+ switch ( $tokens[$i][0] ) {
+ case T_OPEN_TAG:
+ $i++;
+ break;
+ case T_VARIABLE:
+ // '$messages' -> 'messages'
+ $varname = trim( substr( $tokens[$i][1], 1 ) );
+ $varindex = null;
+
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === '[' ) {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $varindex = $this->parseScalar( $tokens[$i] );
+ } else {
+ throw $this->except( $tokens[$i], 'scalar index' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] !== ']' ) {
+ throw $this->except( $tokens[$i], ']' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ }
+
+ if ( $tokens[$i] !== '=' ) {
+ throw $this->except( $tokens[$i], '=' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $buildval = $this->parseScalar( $tokens[$i] );
+ } elseif ( $tokens[$i][0] === T_ARRAY ) {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ if ( $tokens[$i] !== '(' ) {
+ throw $this->except( $tokens[$i], '(' );
+ }
+ $buildval = [];
+ do {
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === ')' ) {
+ break;
+ }
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $key = $this->parseScalar( $tokens[$i] );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i][0] !== T_DOUBLE_ARROW ) {
+ throw $this->except( $tokens[$i], '=>' );
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( isset( $scalarTypes[$tokens[$i][0]] ) ) {
+ $val = $this->parseScalar( $tokens[$i] );
+ }
+ wfSuppressWarnings();
+ $buildval[$key] = $val;
+ wfRestoreWarnings();
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+
+ if ( $tokens[$i] === ',' ) {
+ continue;
+ } elseif ( $tokens[$i] === ')' ) {
+ break;
+ } else {
+ throw $this->except( $tokens[$i], ', or )' );
+ }
+ } while ( true );
+ } else {
+ throw $this->except( $tokens[$i], 'scalar or array' );
+ }
+ if ( is_null( $varindex ) ) {
+ $this->vars[$varname] = $buildval;
+ } else {
+ wfSuppressWarnings();
+ $this->vars[$varname][$varindex] = $buildval;
+ wfRestoreWarnings();
+ }
+ while ( isset( $skipTypes[$tokens[++$i][0]] ) ) {
+ }
+ if ( $tokens[$i] !== ';' ) {
+ throw $this->except( $tokens[$i], ';' );
+ }
+ $i++;
+ break;
+ default:
+ throw $this->except( $tokens[$i], 'open tag, whitespace, or variable.' );
+ }
+ }
+ }
+
+ /**
+ * @param string $got
+ * @param string $expected
+ * @return Exception
+ */
+ private function except( $got, $expected ) {
+ if ( is_array( $got ) ) {
+ $got = token_name( $got[0] ) . " ('" . $got[1] . "')";
+ } else {
+ $got = "'" . $got . "'";
+ }
+
+ return new Exception( "Expected $expected, got $got" );
+ }
+
+ /**
+ * Parse a scalar value in PHP
+ *
+ * @param string $token
+ *
+ * @return mixed Parsed value
+ */
+ function parseScalar( $token ) {
+ if ( is_array( $token ) ) {
+ $str = $token[1];
+ } else {
+ $str = $token;
+ }
+ if ( $str !== '' && $str[0] == '\'' ) {
+ // Single-quoted string
+ // @fixme trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ return strtr( substr( trim( $str ), 1, -1 ),
+ [ '\\\'' => '\'', '\\\\' => '\\' ] );
+ }
+
+ wfSuppressWarnings();
+ if ( $str !== '' && $str[0] == '"' ) {
+ // Double-quoted string
+ // @fixme trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ wfRestoreWarnings();
+ return stripcslashes( substr( trim( $str ), 1, -1 ) );
+ }
+ wfRestoreWarnings();
+
+ if ( substr( $str, 0, 4 ) === 'true' ) {
+ return true;
+ }
+
+ if ( substr( $str, 0, 5 ) === 'false' ) {
+ return false;
+ }
+
+ if ( substr( $str, 0, 4 ) === 'null' ) {
+ return null;
+ }
+
+ // Must be some kind of numeric value, so let PHP's weak typing
+ // be useful for a change
+ return $str;
+ }
+
+ /**
+ * @param string $varname
+ * @return null|string|array
+ */
+ function getVar( $varname ) {
+ if ( isset( $this->vars[$varname] ) ) {
+ return $this->vars[$varname];
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/Updater.php b/www/wiki/extensions/LocalisationUpdate/includes/Updater.php
new file mode 100644
index 00000000..863dc04a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/Updater.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Executes the localisation update.
+ */
+class Updater {
+ /**
+ * Whether the path is a pattern and thus we need to use appropriate
+ * code for fetching directories.
+ *
+ * @param string $path Url
+ * @return bool
+ */
+ public function isDirectory( $path ) {
+ $filename = basename( $path );
+ return strpos( $filename, '*' ) !== false;
+ }
+
+ /**
+ * Expands repository relative path to full url with the given repository
+ * patterns. Extra variables in $info are used as variables and will be
+ * replaced the pattern.
+ *
+ * @param array $info Component information.
+ * @param array $repos Repository information.
+ * @return string
+ */
+ public function expandRemotePath( $info, $repos ) {
+ $pattern = $repos[$info['repo']];
+ unset( $info['repo'], $info['orig'] );
+
+ // This assumes all other keys are used as variables
+ // in the pattern. For example name -> %NAME%.
+ $keys = [];
+ foreach ( array_keys( $info ) as $key ) {
+ $keys[] = '%' . strtoupper( $key ) . '%';
+ }
+
+ $values = array_values( $info );
+ return str_replace( $keys, $values, $pattern );
+ }
+
+ /**
+ * Parses translations from given list of files.
+ *
+ * @param ReaderFactory $readerFactory Factory to construct parsers.
+ * @param array $files List of files with their contents as array values.
+ * @return array List of translations indexed by language code.
+ */
+ public function readMessages( ReaderFactory $readerFactory, array $files ) {
+ $messages = [];
+
+ foreach ( $files as $filename => $contents ) {
+ $reader = $readerFactory->getReader( $filename );
+ try {
+ $parsed = $reader->parse( $contents );
+ } catch ( \Exception $e ) {
+ trigger_error( __METHOD__ . ": Unable to parse messages from $filename", E_USER_WARNING );
+ continue;
+ }
+
+ foreach ( $parsed as $code => $langMessages ) {
+ if ( !isset( $messages[$code] ) ) {
+ $messages[$code] = [];
+ }
+ $messages[$code] = array_merge( $messages[$code], $langMessages );
+ }
+
+ $c = array_sum( array_map( 'count', $parsed ) );
+ // Useful for debugging, maybe create interface to pass this to the script?
+ # echo "$filename with " . get_class( $reader ) . " and $c\n";
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Find new and changed translations in $remote and returns them.
+ *
+ * @param array $origin
+ * @param array $remote
+ * @param array $blacklist Array of message keys to ignore, keys as as array keys.
+ * @return array
+ */
+ public function findChangedTranslations( $origin, $remote, $blacklist = [] ) {
+ $changed = [];
+ foreach ( $remote as $key => $value ) {
+ if ( isset( $blacklist[$key] ) ) {
+ continue;
+ }
+
+ if ( !isset( $origin[$key] ) || $value !== $origin[$key] ) {
+ $changed[$key] = $value;
+ }
+ }
+ return $changed;
+ }
+
+ /**
+ * Fetches files from given Url pattern.
+ *
+ * @param FetcherFactory $factory Factory to construct fetchers.
+ * @param string $path Url to the file or pattern of files.
+ * @return array List of Urls with file contents as path.
+ */
+ public function fetchFiles( FetcherFactory $factory, $path ) {
+ $fetcher = $factory->getFetcher( $path );
+
+ if ( $this->isDirectory( $path ) ) {
+ $files = $fetcher->fetchDirectory( $path );
+ } else {
+ $files = [ $path => $fetcher->fetchFile( $path ) ];
+ }
+
+ // Remove files which were not found
+ return array_filter( $files );
+ }
+
+ public function execute(
+ Finder $finder,
+ ReaderFactory $readerFactory,
+ FetcherFactory $fetcherFactory,
+ array $repos,
+ $logger
+ ) {
+ $components = $finder->getComponents();
+
+ $updatedMessages = [];
+
+ foreach ( $components as $key => $info ) {
+ $logger->logInfo( "Updating component $key" );
+
+ $originFiles = $this->fetchFiles( $fetcherFactory, $info['orig'] );
+ $remotePath = $this->expandRemotePath( $info, $repos );
+ try {
+ $remoteFiles = $this->fetchFiles( $fetcherFactory, $remotePath );
+ } catch ( \Exception $e ) {
+ $logger->logError( __METHOD__ . ": Unable to fetch messages from $remotePath" );
+ continue;
+ }
+
+ if ( $remoteFiles === [] ) {
+ // Small optimization: if nothing to compare with, skip
+ continue;
+ }
+
+ $originMessages = $this->readMessages( $readerFactory, $originFiles );
+ $remoteMessages = $this->readMessages( $readerFactory, $remoteFiles );
+
+ if ( !isset( $remoteMessages['en'] ) ) {
+ // Could not find remote messages
+ continue;
+ }
+
+ // If remote translation in English is not present or differs, we do not want
+ // translations for other languages for those messages, as they are either not
+ // used in this version of code or can be incompatible.
+ $forbiddenKeys = $this->findChangedTranslations(
+ $originMessages['en'],
+ $remoteMessages['en']
+ );
+
+ // We never accept updates for English strings
+ unset( $originMessages['en'], $remoteMessages['en'] );
+
+ // message: string in all languages; translation: string in one language.
+ foreach ( $remoteMessages as $language => $remoteTranslations ) {
+ // Check for completely new languages
+ $originTranslations = [];
+ if ( isset( $originMessages[$language] ) ) {
+ $originTranslations = $originMessages[$language];
+ }
+
+ $updatedTranslations = $this->findChangedTranslations(
+ $originTranslations,
+ $remoteTranslations,
+ $forbiddenKeys
+ );
+
+ // Avoid empty arrays
+ if ( $updatedTranslations === [] ) {
+ continue;
+ }
+
+ if ( !isset( $updatedMessages[$language] ) ) {
+ $updatedMessages[$language] = [];
+ }
+
+ // In case of conflicts, which should not exist, this prefers the
+ // first translation seen.
+ $updatedMessages[$language] += $updatedTranslations;
+ }
+ }
+
+ return $updatedMessages;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/fetcher/Fetcher.php b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/Fetcher.php
new file mode 100644
index 00000000..62febac8
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/Fetcher.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for classes which fetch files over different protocols and ways.
+ */
+interface Fetcher {
+ /**
+ * Fetches a single resource.
+ *
+ * @param string $url
+ * @return bool|string False on failure.
+ */
+ public function fetchFile( $url );
+
+ /**
+ * Fetch a list of resources. This has the benefit of being able to pick up
+ * new languages as they appear if languages are stored in separate files.
+ *
+ * @param string $pattern
+ * @return array
+ */
+ public function fetchDirectory( $pattern );
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FetcherFactory.php b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FetcherFactory.php
new file mode 100644
index 00000000..9273c935
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FetcherFactory.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Constructs fetchers based on the repository urls.
+ */
+class FetcherFactory {
+ public function getFetcher( $path ) {
+ if ( strpos( $path, 'https://raw.github.com/' ) === 0 ) {
+ return new GitHubFetcher();
+ } elseif ( strpos( $path, 'http://' ) === 0 ) {
+ return new HttpFetcher();
+ } elseif ( strpos( $path, 'https://' ) === 0 ) {
+ return new HttpFetcher();
+ } else {
+ return new FileSystemFetcher();
+ }
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FileSystemFetcher.php b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FileSystemFetcher.php
new file mode 100644
index 00000000..43daa65d
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/FileSystemFetcher.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Accesses file system directly.
+ */
+class FileSystemFetcher implements Fetcher {
+ /**
+ * @param string $url
+ *
+ * @return bool|string
+ */
+ public function fetchFile( $url ) {
+ // Remove the protocol prefix
+ $url = preg_replace( '~^file://~', '', $url );
+
+ if ( !is_readable( $url ) ) {
+ return false;
+ }
+
+ return file_get_contents( $url );
+ }
+
+ /**
+ * @param string $pattern
+ *
+ * @return array
+ */
+ public function fetchDirectory( $pattern ) {
+ // Remove the protocol prefix
+ $pattern = preg_replace( '~^file://~', '', $pattern );
+
+ $data = [];
+ foreach ( glob( $pattern ) as $file ) {
+ if ( is_readable( $file ) ) {
+ $data["file://$file"] = file_get_contents( $file );
+ }
+ }
+ return $data;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/fetcher/GitHubFetcher.php b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/GitHubFetcher.php
new file mode 100644
index 00000000..f8bea9c1
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/GitHubFetcher.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * This class uses GitHub api to obtain a list of files present in a directory
+ * to avoid fetching files that don't exist.
+ *
+ * @todo Could use file hashes to 1) avoid fetching files with same hash as
+ * the source. 2) avoid fetching files which haven't changed since last check
+ * if we store them.
+ */
+class GitHubFetcher extends HttpFetcher {
+ /**
+ * @param string $pattern
+ *
+ * @return array
+ * @throws \Exception
+ */
+ public function fetchDirectory( $pattern ) {
+ $domain = preg_quote( 'https://raw.github.com/', '~' );
+ $p = "~^$domain(?P<org>[^/]+)/(?P<repo>[^/]+)/(?P<branch>[^/]+)/(?P<path>.+)/.+$~";
+ preg_match( $p, $pattern, $m );
+
+ $apiURL = "https://api.github.com/repos/{$m['org']}/{$m['repo']}/contents/{$m['path']}";
+ $json = \Http::get( $apiURL );
+ if ( !$json ) {
+ throw new \Exception( "Unable to get directory listing for {$m['org']}/{$m['repo']}" );
+ }
+
+ $files = [];
+ $json = \FormatJson::decode( $json, true );
+ foreach ( $json as $fileinfo ) {
+ $fileurl = dirname( $pattern ) . '/' . $fileinfo['name'];
+ $file = $this->fetchFile( $fileurl );
+ if ( $file ) {
+ $files[$fileurl] = $file;
+ }
+ }
+ return $files;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/fetcher/HttpFetcher.php b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/HttpFetcher.php
new file mode 100644
index 00000000..72988e98
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/fetcher/HttpFetcher.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Fetches files over HTTP(s).
+ */
+class HttpFetcher implements Fetcher {
+ /**
+ * @param string $url
+ *
+ * @return bool|string
+ */
+ public function fetchFile( $url ) {
+ return \Http::get( $url );
+ }
+
+ /**
+ * This is horribly inefficient. Subclasses have more efficient
+ * implementation of this.
+ * @param string $pattern
+ * @return array
+ */
+ public function fetchDirectory( $pattern ) {
+ $files = [];
+
+ $languages = \Language::fetchLanguageNames( null, 'mwfile' );
+
+ foreach ( array_keys( $languages ) as $code ) {
+ // Hack for core
+ if ( strpos( $pattern, 'Messages*.php' ) !== false ) {
+ $code = ucfirst( strtr( $code, '-', '_' ) );
+ }
+
+ $url = str_replace( '*', $code, $pattern );
+ $file = $this->fetchFile( $url );
+ if ( $file ) {
+ $files[$url] = $file;
+ }
+ }
+
+ return $files;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/finder/Finder.php b/www/wiki/extensions/LocalisationUpdate/includes/finder/Finder.php
new file mode 100644
index 00000000..acebc42a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/finder/Finder.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for classes which provide list of components, which should be
+ * included for l10n updates.
+ */
+class Finder {
+
+ /**
+ * @var array
+ */
+ private $php;
+
+ /**
+ * @var array
+ */
+ private $json;
+
+ /**
+ * @var string
+ */
+ private $core;
+ /**
+ * @param array $php See $wgExtensionMessagesFiles
+ * @param array $json See $wgMessagesDirs
+ * @param string $core Absolute path to MediaWiki core
+ */
+ public function __construct( $php, $json, $core ) {
+ $this->php = $php;
+ $this->json = $json;
+ $this->core = $core;
+ }
+
+ /**
+ * @return array
+ */
+ public function getComponents() {
+ $components = [];
+
+ // For older versions of Mediawiki, pull json updates even though its still using php
+ if ( !isset( $this->json['core'] ) ) {
+ $components['core'] = [
+ 'repo' => 'mediawiki',
+ 'orig' => "file://{$this->core}/languages/messages/Messages*.php",
+ 'path' => 'languages/messages/i18n/*.json',
+ ];
+ }
+
+ foreach ( $this->json as $key => $value ) {
+ // Json should take priority if both exist
+ unset( $this->php[$key] );
+
+ foreach ( (array)$value as $subkey => $subvalue ) {
+ // Mediawiki core files
+ $matches = [];
+ if ( preg_match( '~/(?P<path>(?:includes|languages|resources)/.*)$~', $subvalue, $matches ) ) {
+ $components["$key-$subkey"] = [
+ 'repo' => 'mediawiki',
+ 'orig' => "file://$value/*.json",
+ 'path' => "{$matches['path']}/*.json",
+ ];
+ continue;
+ }
+
+ $item = $this->getItem( 'extensions', $subvalue );
+ if ( $item !== null ) {
+ $item['repo'] = 'extension';
+ $components["$key-$subkey"] = $item;
+ continue;
+ }
+
+ $item = $this->getItem( 'skins', $subvalue );
+ if ( $item !== null ) {
+ $item['repo'] = 'skin';
+ $components["$key-$subkey"] = $item;
+ continue;
+ }
+ }
+ }
+
+ foreach ( $this->php as $key => $value ) {
+ $matches = [];
+ $ok = preg_match( '~/extensions/(?P<name>[^/]+)/(?P<path>.*\.i18n\.php)$~', $value, $matches );
+ if ( !$ok ) {
+ continue;
+ }
+
+ $components[$key] = [
+ 'repo' => 'extension',
+ 'name' => $matches['name'],
+ 'orig' => "file://$value",
+ 'path' => $matches['path'],
+ ];
+ }
+
+ return $components;
+ }
+
+ /**
+ * @param string $dir extensions or skins
+ * @param string $subvalue
+ * @return array|null
+ */
+ private function getItem( $dir, $subvalue ) {
+ // This ignores magic, alias etc. non message files
+ $matches = [];
+ if ( !preg_match( "~/$dir/(?P<name>[^/]+)/(?P<path>.*)$~", $subvalue, $matches ) ) {
+ return null;
+ }
+
+ return [
+ 'name' => $matches['name'],
+ 'orig' => "file://$subvalue/*.json",
+ 'path' => "{$matches['path']}/*.json",
+ ];
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/reader/JSONReader.php b/www/wiki/extensions/LocalisationUpdate/includes/reader/JSONReader.php
new file mode 100644
index 00000000..e8613660
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/reader/JSONReader.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Reads MediaWiki JSON i18n files.
+ */
+class JSONReader implements Reader {
+ /// @var string Language tag
+ protected $code;
+
+ public function __construct( $code = null ) {
+ $this->code = $code;
+ }
+
+ /**
+ * @param string $contents
+ *
+ * @return array
+ */
+ public function parse( $contents ) {
+ $messages = \FormatJson::decode( $contents, true );
+ unset( $messages['@metadata'] );
+
+ if ( $this->code ) {
+ return [ $this->code => $messages ];
+ }
+
+ // Assuming that the array is keyed by language codes
+ return $messages;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/reader/PHPReader.php b/www/wiki/extensions/LocalisationUpdate/includes/reader/PHPReader.php
new file mode 100644
index 00000000..43e4db25
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/reader/PHPReader.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Reads MediaWiki PHP i18n files.
+ */
+class PHPReader implements Reader {
+ /// @var string Language tag
+ protected $code;
+
+ public function __construct( $code = null ) {
+ $this->code = $code;
+ }
+
+ /**
+ * @param string $contents
+ *
+ * @return array
+ */
+ public function parse( $contents ) {
+ if ( strpos( $contents, '$messages' ) === false ) {
+ // This happens for some core languages that only have a fallback.
+ return [];
+ }
+
+ $php = $this->cleanupFile( $contents );
+ $reader = new \QuickArrayReader( "<?php $php" );
+ $messages = $reader->getVar( 'messages' );
+
+ if ( $this->code ) {
+ return [ $this->code => $messages ];
+ }
+
+ // Assuming that the array is keyed by language codes
+ return $messages;
+ }
+
+ /**
+ * Removes all unneeded content from a file and returns it.
+ *
+ * @param string $contents String
+ * @return string PHP code without PHP tags
+ */
+ protected function cleanupFile( $contents ) {
+ // We hate the windows vs linux linebreaks.
+ $contents = preg_replace( '/\r\n?/', "\n", $contents );
+
+ // We only want message arrays.
+ $results = [];
+ preg_match_all( '/\$messages(?:.*\s)*?\);/', $contents, $results );
+
+ // But we want them all in one string.
+ return implode( "\n\n", $results[0] );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/reader/Reader.php b/www/wiki/extensions/LocalisationUpdate/includes/reader/Reader.php
new file mode 100644
index 00000000..fd37322e
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/reader/Reader.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for file readers.
+ */
+interface Reader {
+ /**
+ * Returns a list of messages indexed by language code. Example
+ * array( 'en' => array( 'key' => 'value' ) );
+ * @param string $contents File contents as a string.
+ * @return array
+ */
+ public function parse( $contents );
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/includes/reader/ReaderFactory.php b/www/wiki/extensions/LocalisationUpdate/includes/reader/ReaderFactory.php
new file mode 100644
index 00000000..44bc5e40
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/includes/reader/ReaderFactory.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Constructs readers for files based on the names.
+ */
+class ReaderFactory {
+ /**
+ * Constructs a suitable reader for a given path.
+ * @param string $filename Usually a relative path to the file name.
+ * @return Reader
+ * @throws Exception
+ */
+ public function getReader( $filename ) {
+ if ( preg_match( '/i18n\.php$/', $filename ) ) {
+ return new PHPReader();
+ }
+
+ // Ugly hack for core i18n files
+ if ( preg_match( '/Messages(.*)\.php$/', $filename ) ) {
+ $code = \Language::getCodeFromFileName( basename( $filename ), 'Messages' );
+ return new PHPReader( $code );
+ }
+
+ if ( preg_match( '/\.json/', $filename ) ) {
+ $code = basename( $filename, '.json' );
+ return new JSONReader( $code );
+ }
+
+ throw new \Exception( "Unknown file format: " . $filename );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/phpcs.xml b/www/wiki/extensions/LocalisationUpdate/phpcs.xml
new file mode 100644
index 00000000..32816b38
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/phpcs.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<ruleset>
+ <rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingParamComment" />
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingParamName" />
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingParamTag" />
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingReturn" />
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic" />
+ <exclude name="MediaWiki.Files.ClassMatchesFilename.NotMatch" />
+ <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment" />
+ </rule>
+ <file>.</file>
+ <arg name="extensions" value="php,php5,inc" />
+ <arg name="encoding" value="UTF-8" />
+</ruleset>
diff --git a/www/wiki/extensions/LocalisationUpdate/reader/JSONReader.php b/www/wiki/extensions/LocalisationUpdate/reader/JSONReader.php
new file mode 100644
index 00000000..fdc4e1d6
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/reader/JSONReader.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Reads MediaWiki JSON i18n files.
+ */
+class JSONReader implements Reader {
+ /// @var string Language tag
+ protected $code;
+
+ public function __construct( $code = null ) {
+ $this->code = $code;
+ }
+
+ public function parse( $contents ) {
+ $messages = \FormatJson::decode( $contents, true );
+ unset( $messages['@metadata'] );
+
+ if ( $this->code ) {
+ return [ $this->code => $messages ];
+ }
+
+ // Assuming that the array is keyed by language codes
+ return $messages;
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/reader/PHPReader.php b/www/wiki/extensions/LocalisationUpdate/reader/PHPReader.php
new file mode 100644
index 00000000..f314744b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/reader/PHPReader.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Reads MediaWiki PHP i18n files.
+ */
+class PHPReader implements Reader {
+ /// @var string Language tag
+ protected $code;
+
+ public function __construct( $code = null ) {
+ $this->code = $code;
+ }
+
+ public function parse( $contents ) {
+ if ( strpos( $contents, '$messages' ) === false ) {
+ // This happens for some core languages that only have a fallback.
+ return [];
+ }
+
+ $php = $this->cleanupFile( $contents );
+ $reader = new \QuickArrayReader( "<?php $php" );
+ $messages = $reader->getVar( 'messages' );
+
+ if ( $this->code ) {
+ return [ $this->code => $messages ];
+ }
+
+ // Assuming that the array is keyed by language codes
+ return $messages;
+ }
+
+ /**
+ * Removes all unneeded content from a file and returns it.
+ *
+ * @param string $contents String
+ * @return string PHP code without PHP tags
+ */
+ protected function cleanupFile( $contents ) {
+ // We hate the windows vs linux linebreaks.
+ $contents = preg_replace( '/\r\n?/', "\n", $contents );
+
+ // We only want message arrays.
+ $results = [];
+ preg_match_all( '/\$messages(?:.*\s)*?\);/', $contents, $results );
+
+ // But we want them all in one string.
+ return implode( "\n\n", $results[0] );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/reader/Reader.php b/www/wiki/extensions/LocalisationUpdate/reader/Reader.php
new file mode 100644
index 00000000..8c263ff0
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/reader/Reader.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Interface for file readers.
+ */
+interface Reader {
+ /**
+ * Returns a list of messages indexed by language code. Example
+ * array( 'en' => array( 'key' => 'value' ) );
+ * @param string $contents File contents as a string.
+ * @return array
+ */
+ public function parse( $contents );
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/reader/ReaderFactory.php b/www/wiki/extensions/LocalisationUpdate/reader/ReaderFactory.php
new file mode 100644
index 00000000..ab5cdf1b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/reader/ReaderFactory.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * Constructs readers for files based on the names.
+ */
+class ReaderFactory {
+ /**
+ * Constructs a suitable reader for a given path.
+ * @param string $filename Usually a relative path to the file name.
+ * @return Reader
+ * @throw Exception
+ */
+ public function getReader( $filename ) {
+ if ( preg_match( '/i18n\.php$/', $filename ) ) {
+ return new PHPReader();
+ }
+
+ // Ugly hack for core i18n files
+ if ( preg_match( '/Messages(.*)\.php$/', $filename ) ) {
+ $code = \Language::getCodeFromFileName( basename( $filename ), 'Messages' );
+ return new PHPReader( $code );
+ }
+
+ if ( preg_match( '/\.json/', $filename ) ) {
+ $code = basename( $filename, '.json' );
+ return new JSONReader( $code );
+ }
+
+ throw new \Exception( "Unknown file format: " . $filename );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phan/config.php b/www/wiki/extensions/LocalisationUpdate/tests/phan/config.php
new file mode 100644
index 00000000..f2660b8a
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phan/config.php
@@ -0,0 +1,3 @@
+<?php
+
+return require __DIR__ . '/../../vendor/mediawiki/mediawiki-phan-config/src/config.php';
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phpunit/Makefile b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/Makefile
new file mode 100644
index 00000000..e98c12ca
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/Makefile
@@ -0,0 +1,12 @@
+ifndef MW_INSTALL_PATH
+ MW_INSTALL_PATH=../../../..
+endif
+
+DIRS=reader
+
+default:
+ php ${MW_INSTALL_PATH}/tests/phpunit/phpunit.php .
+
+.PHONY: *Test.php $(DIRS)
+*Test.php $(DIRS):
+ php ${MW_INSTALL_PATH}/tests/phpunit/phpunit.php $@
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php
new file mode 100644
index 00000000..00c45e12
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/UpdaterTest.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+use PHPUnit4And6Compat;
+
+/**
+ * @covers \LocalisationUpdate\Updater
+ */
+class UpdaterTest extends \PHPUnit\Framework\TestCase {
+ use PHPUnit4And6Compat;
+
+ public function testIsDirectory() {
+ $updater = new Updater();
+
+ $this->assertTrue(
+ $updater->isDirectory( '/IP/extensions/Translate/i18n/*.json' ),
+ 'Extension json files are a file pattern'
+ );
+
+ $this->assertFalse(
+ $updater->isDirectory( '/IP/extensions/Translate/Translate.i18n.php' ),
+ 'Extension php file is not a pattern'
+ );
+ }
+
+ public function testExpandRemotePath() {
+ $updater = new Updater();
+ $repos = [ 'main' => 'file:///repos/%NAME%/%SOME-VAR%' ];
+
+ $info = [
+ 'repo' => 'main',
+ 'name' => 'product',
+ 'some-var' => 'file',
+ ];
+ $this->assertEquals(
+ 'file:///repos/product/file',
+ $updater->expandRemotePath( $info, $repos ),
+ 'Variables are expanded correctly'
+ );
+ }
+
+ public function testReadMessages() {
+ $updater = $updater = new Updater();
+
+ $input = [ 'file' => 'Hello World!' ];
+ $output = [ 'en' => [ 'key' => $input['file'] ] ];
+
+ $reader = $this->getMock( 'LocalisationUpdate\Reader' );
+ $reader
+ ->expects( $this->once() )
+ ->method( 'parse' )
+ ->will( $this->returnValue( $output ) );
+
+ $factory = $this->getMock( 'LocalisationUpdate\ReaderFactory' );
+ $factory
+ ->expects( $this->once() )
+ ->method( 'getReader' )
+ ->will( $this->returnValue( $reader ) );
+
+ $observed = $updater->readMessages( $factory, $input );
+ $this->assertEquals( $output, $observed, 'Tries to parse given file' );
+ }
+
+ public function testFindChangedTranslations() {
+ $updater = $updater = new Updater();
+
+ $origin = [
+ 'A' => '1',
+ 'C' => '3',
+ 'D' => '4',
+ ];
+ $remote = [
+ 'A' => '1', // No change key
+ 'B' => '2', // New key
+ 'C' => '33', // Changed key
+ 'D' => '44', // Blacklisted key
+ ];
+ $blacklist = [ 'D' => 0 ];
+ $expected = [ 'B' => '2', 'C' => '33' ];
+ $observed = $updater->findChangedTranslations( $origin, $remote, $blacklist );
+ $this->assertEquals( $expected, $observed, 'Changed and new keys returned' );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php
new file mode 100644
index 00000000..1b4db0f4
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/finder/FinderTest.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * @covers \LocalisationUpdate\Finder
+ */
+class FinderTest extends \PHPUnit\Framework\TestCase {
+ public function testGetComponents() {
+ $finder = new Finder(
+ [
+ 'TranslateSearch' => '/IP/extensions/Translate/TranslateSearch.i18n.php',
+ 'Babel' => '/IP/extensions/Babel/Babel.i18n.php',
+ ],
+ [
+ 'Babel' => '/IP/extensions/Babel/i18n',
+ 'Door' => [
+ 'core' => '/IP/extensions/Door/i18n/core',
+ 'extra' => '/IP/extensions/Door/i18n/extra',
+ ],
+ 'Vector' => '/IP/skins/Vector/i18n',
+ ],
+ '/IP'
+ );
+ $observed = $finder->getComponents();
+
+ $expected = [
+ 'repo' => 'mediawiki',
+ 'orig' => "file:///IP/languages/messages/Messages*.php",
+ 'path' => 'languages/messages/i18n/*.json',
+ ];
+
+ $this->assertArrayHasKey( 'core', $observed );
+ $this->assertEquals( $expected, $observed['core'], 'Core php file' );
+
+ $expected = [
+ 'repo' => 'extension',
+ 'name' => 'Translate',
+ 'orig' => 'file:///IP/extensions/Translate/TranslateSearch.i18n.php',
+ 'path' => 'TranslateSearch.i18n.php'
+ ];
+ $this->assertArrayHasKey( 'TranslateSearch', $observed );
+ $this->assertEquals( $expected, $observed['TranslateSearch'], 'PHP only extension' );
+
+ $expected = [
+ 'repo' => 'extension',
+ 'name' => 'Babel',
+ 'orig' => 'file:///IP/extensions/Babel/i18n/*.json',
+ 'path' => 'i18n/*.json'
+ ];
+ $this->assertArrayHasKey( 'Babel-0', $observed );
+ $this->assertEquals( $expected, $observed['Babel-0'], 'PHP&JSON extension' );
+
+ $expected = [
+ 'repo' => 'extension',
+ 'name' => 'Door',
+ 'orig' => 'file:///IP/extensions/Door/i18n/core/*.json',
+ 'path' => 'i18n/core/*.json'
+ ];
+ $this->assertArrayHasKey( 'Door-core', $observed );
+ $this->assertEquals( $expected, $observed['Door-core'], 'Multidir json extension' );
+
+ $expected = [
+ 'repo' => 'extension',
+ 'name' => 'Door',
+ 'orig' => 'file:///IP/extensions/Door/i18n/extra/*.json',
+ 'path' => 'i18n/extra/*.json'
+ ];
+ $this->assertArrayHasKey( 'Door-extra', $observed );
+ $this->assertEquals( $expected, $observed['Door-extra'], 'Multidir json extension' );
+
+ $expected = [
+ 'repo' => 'skin',
+ 'name' => 'Vector',
+ 'orig' => 'file:///IP/skins/Vector/i18n/*.json',
+ 'path' => 'i18n/*.json'
+ ];
+ $this->assertArrayHasKey( 'Vector-0', $observed );
+ $this->assertEquals( $expected, $observed['Vector-0'], 'Json skin' );
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php
new file mode 100644
index 00000000..11f0a03b
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/JSONReaderTest.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * @covers \LocalisationUpdate\JSONReader
+ */
+class JSONReaderTest extends \PHPUnit\Framework\TestCase {
+ /**
+ * @dataProvider parseProvider
+ */
+ public function testParse( $input, $expected, $comment ) {
+ $reader = new JSONReader( 'xx' );
+ $observed = $reader->parse( $input );
+ $this->assertEquals( $expected, $observed['xx'], $comment );
+ }
+
+ public function parseProvider() {
+ return [
+ [
+ '{}',
+ [],
+ 'empty file',
+ ],
+ [
+ '{"key":"value"}',
+ [ 'key' => 'value' ],
+ 'file with one string',
+ ],
+ [
+ '{"@metadata":{"authors":["Nike"]},"key":"value2"}',
+ [ 'key' => 'value2' ],
+ '@metadata is ignored',
+ ]
+ ];
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php
new file mode 100644
index 00000000..86776395
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/tests/phpunit/reader/ReaderFactoryTest.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+namespace LocalisationUpdate;
+
+/**
+ * @covers \LocalisationUpdate\ReaderFactory
+ */
+class ReaderFactoryTest extends \PHPUnit\Framework\TestCase {
+ /**
+ * @dataProvider getReaderProvider
+ */
+ public function testGetReader( $input, $expected, $comment ) {
+ $factory = new ReaderFactory();
+ $reader = $factory->getReader( $input );
+ $observed = get_class( $reader );
+ $this->assertEquals( $expected, $observed, $comment );
+ }
+
+ public function getReaderProvider() {
+ return [
+ [
+ 'languages/messages/MessagesFi.php',
+ 'LocalisationUpdate\PHPReader',
+ 'core php file',
+ ],
+ [
+ 'extensions/Translate/Translate.i18n.php',
+ 'LocalisationUpdate\PHPReader',
+ 'extension php file',
+ ],
+ [
+ 'extension/Translate/i18n/core/de.json',
+ 'LocalisationUpdate\JSONReader',
+ 'extension json file',
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/extensions/LocalisationUpdate/update.php b/www/wiki/extensions/LocalisationUpdate/update.php
new file mode 100644
index 00000000..a8ed7ead
--- /dev/null
+++ b/www/wiki/extensions/LocalisationUpdate/update.php
@@ -0,0 +1,93 @@
+<?php
+
+$IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
+ ? getenv( 'MW_INSTALL_PATH' )
+ : realpath( __DIR__ . '/../../' );
+
+require "$IP/maintenance/Maintenance.php";
+
+class Update extends Maintenance {
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = 'Fetches translation updates to MediaWiki core, skins and extensions.';
+ $this->addOption(
+ 'repoid',
+ 'Fetch translations from repositories identified by this',
+ false, /*required*/
+ true /*has arg*/
+ );
+
+ $this->requireExtension( 'LocalisationUpdate' );
+ }
+
+ public function execute() {
+ // Prevent the script from timing out
+ set_time_limit( 0 );
+ ini_set( "max_execution_time", 0 );
+ ini_set( 'memory_limit', -1 );
+
+ global $IP;
+ global $wgExtensionMessagesFiles;
+ global $wgLocalisationUpdateRepositories;
+ global $wgLocalisationUpdateRepository;
+
+ $dir = LocalisationUpdate::getDirectory();
+ if ( !$dir ) {
+ $this->error( "No cache directory configured", true );
+ return;
+ }
+
+ $lc = Language::getLocalisationCache();
+ $messagesDirs = $lc->getMessagesDirs();
+
+ $finder = new LocalisationUpdate\Finder( $wgExtensionMessagesFiles, $messagesDirs, $IP );
+ $readerFactory = new LocalisationUpdate\ReaderFactory();
+ $fetcherFactory = new LocalisationUpdate\FetcherFactory();
+
+ $repoid = $this->getOption( 'repoid', $wgLocalisationUpdateRepository );
+ if ( !isset( $wgLocalisationUpdateRepositories[$repoid] ) ) {
+ $known = implode( ', ', array_keys( $wgLocalisationUpdateRepositories ) );
+ $this->error( "Unknown repoid $repoid; known: $known", true );
+ return;
+ }
+ $repos = $wgLocalisationUpdateRepositories[$repoid];
+
+ // output and error methods are protected, hence we add logInfo and logError
+ // public methods, that hopefully won't conflict in the future with the base class.
+ $logger = $this;
+
+ // Do it ;)
+ $updater = new LocalisationUpdate\Updater();
+ $updatedMessages = $updater->execute(
+ $finder,
+ $readerFactory,
+ $fetcherFactory,
+ $repos,
+ $logger
+ );
+
+ // Store it ;)
+ $count = array_sum( array_map( 'count', $updatedMessages ) );
+ if ( !$count ) {
+ $this->output( "Found no new translations\n" );
+ return;
+ }
+
+ foreach ( $updatedMessages as $language => $messages ) {
+ $filename = "$dir/" . LocalisationUpdate::getFilename( $language );
+ file_put_contents( $filename, FormatJson::encode( $messages, true ) );
+ }
+ $this->output( "Saved $count new translations\n" );
+ }
+
+ public function logInfo( $msg ) {
+ $this->output( $msg . "\n" );
+ }
+
+ public function logError( $msg ) {
+ $this->error( $msg );
+ }
+}
+
+$maintClass = Update::class;
+require_once RUN_MAINTENANCE_IF_MAIN;