├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── PERFORMANCE.md
├── README.md
├── benchmark-apcu.php
├── composer.json
├── phpunit.xml.dist
├── psalm-baseline.xml
├── psalm.xml
├── src
├── Cache
│ ├── ApcuCache.php
│ ├── ApcuCacheFactory.php
│ ├── CacheFactoryInterface.php
│ ├── CacheInterface.php
│ ├── GetAllInterface.php
│ └── InMemoryCache.php
├── CacheException.php
├── Loader.php
├── MoParser.php
├── ReaderException.php
├── StringReader.php
├── Translator.php
└── functions.php
└── tests
├── Cache
├── ApcuCacheFactoryTest.php
├── ApcuCacheTest.php
├── ApcuDisabledTest.php
└── InMemoryCacheTest.php
├── FunctionsTest.php
├── LoaderTest.php
├── MoFilesTest.php
├── PluralFormulaTest.php
├── PluralTest.php
├── StringReaderTest.php
├── TranslatorTest.php
└── data
├── big.mo
├── error
├── big.mo
├── dos.mo
├── empty.mo
├── fpd.mo
├── fpdle.mo
└── magic.mo
├── invalid-formula.mo
├── lessplurals.mo
├── little.mo
├── locale
├── be
│ └── LC_MESSAGES
│ │ └── phpmyadmin.mo
├── be@latin
│ └── LC_MESSAGES
│ │ └── phpmyadmin.mo
└── cs
│ └── LC_MESSAGES
│ └── phpmyadmin.mo
├── noheader.mo
├── not-translated
├── fpd1.mo
└── invalid-equation.mo
└── plurals.mo
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [5.4.0] - 2025-03-16
4 |
5 | * Drop support for PHP 7.2, PHP 7.3, PHP 7.4, PHP 8.0 and PHP 8.1.
6 |
7 | ## [Unreleased]
8 |
9 | * Bump PHP minimum version to 7.2
10 | * Add support for Symfony 7
11 |
12 | ## [5.3.1] - 2023-08-23
13 |
14 | * Add function guards to the global functions (#44)
15 |
16 | ## [5.3.0] - 2022-04-26
17 |
18 | * Add support for Symfony 6
19 | * Split out `.mo` parsing to separate `MoParser` class
20 | * Added `CacheInterface` so alternate cache implementations are pluggable
21 | * Added `ApcuCache` implementation to leverage shared in-memory translation cache
22 |
23 | ## [5.2.0] - 2021-02-05
24 |
25 | * Fix "Translator::selectString() must be of the type integer, boolean returned" (#37)
26 | * Fix "TypeError: StringReader::readintarray() ($count) must be of type int, float given" failing tests on ARM (#36)
27 | * Add support for getting and setting all translations (#30)
28 |
29 | ## [5.1.0] - 2020-11-15
30 |
31 | * Allow PHPUnit 9 (#35)
32 | * Fix some typos
33 | * Sync config files
34 | * Allow PHP 8.0
35 |
36 | ## [5.0.0] - 2020-02-28
37 |
38 | * Drop support for PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, PHP 7.0 and HHVM
39 | * Enabled strict mode on PHP files
40 | * Add support for Symfony 5 (#34)
41 | * Add support for phpunit 8
42 | * Rename CHANGES.md to CHANGELOG.md and follow a standard format
43 |
44 | ## [4.0] - 2018-02-12
45 |
46 | * The library no longer changes system locales.
47 |
48 | ## [3.4] - 2017-12-15
49 |
50 | * Added Translator::setTranslation method.
51 |
52 | ## [3.3] - 2017-06-01
53 |
54 | * Add support for switching locales for Loader instance.
55 |
56 | ## [3.2] - 2017-05-23
57 |
58 | * Various fixes when handling corrupted mo files.
59 |
60 | ## [3.1] - 2017-05-15
61 |
62 | * Documentation improvements.
63 |
64 | ## [3.0] - 2017-01-23
65 |
66 | * All classes moved to the PhpMyAdmin namespace.
67 |
68 | ## [2.2] - 2017-01-07
69 |
70 | * Coding style cleanup.
71 | * Avoid installing tests using composer.
72 |
73 | ## [2.1] - 2016-12-21
74 |
75 | * Various code cleanups.
76 | * Added support for PHP 5.3.
77 |
78 | ## [2.0] - 2016-10-13
79 |
80 | * Consistently use camelCase in API.
81 | * No more relies on using eval().
82 | * Depends on symfony/expression-language for calculations.
83 |
84 | ## [1.2] - 2016-09-22
85 |
86 | * Stricter validation of plural expression.
87 |
88 | ## [1.1] - 2016-08-29
89 |
90 | * Improved handling of corrupted mo files.
91 | * Minor performance improvements.
92 | * Stricter validation of plural expression.
93 |
94 | ## [1.0] - 2016-04-27
95 |
96 | * Documentation improvements.
97 | * Testsuite improvements.
98 |
99 | ## [0.4] - 2016-03-02
100 |
101 | * Fixed test failures with hhvm due to broken putenv.
102 |
103 | ## [0.3] - 2016-03-01
104 |
105 | * Added Loader::detectlocale method.
106 |
107 | ## [0.2] - 2016-02-24
108 |
109 | * Marked PHP 5.4 and 5.5 as supported.
110 |
111 | ## [0.1] - 2016-02-23
112 |
113 | * Initial release.
114 |
115 | [5.4.0]: https://github.com/phpmyadmin/motranslator/compare/5.3.1...5.4.0
116 | [5.3.1]: https://github.com/phpmyadmin/motranslator/compare/5.3.0...5.3.1
117 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at info@phpmyadmin.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to motranslator
2 |
3 | ## Reporting issues
4 |
5 | Our issue tracker is hosted at GitHub:
6 |
7 | https://github.com/phpmyadmin/motranslator/issues
8 |
9 | Please search for existing issues before reporting new ones.
10 |
11 | ## Working with Git checkout
12 |
13 | The dependencies are managed by Composer, to get them all installed (or update
14 | on consequent runs) do:
15 |
16 | ```
17 | composer update
18 | ```
19 |
20 | ## Submitting patches
21 |
22 | Please submit your patches using GitHub pull requests, this allows us to review
23 | them and to run automated tests on the code.
24 |
25 | ## Coding standards
26 |
27 | We do follow PSR-1 and PSR-2 coding standards.
28 |
29 | You can use phpcbf to fix the code to match our expectations:
30 |
31 | ```
32 | composer run phpcbf
33 | ```
34 |
35 | ## Testsuite
36 |
37 | Our code comes with quite comprehensive testsuite, it is automatically executed
38 | on every commit and pull request, you can also run it locally:
39 |
40 | ```
41 | composer run phpunit
42 | ```
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/PERFORMANCE.md:
--------------------------------------------------------------------------------
1 | # Performance
2 |
3 | This library was tweaked for best performance for single use - translating
4 | application with many strings using mo file. Current benchmarks show it's about
5 | four times faster than original php-gettext.
6 |
7 | There are two benchmark scripts in the code:
8 |
9 | * ``benchmark-context.php`` - benchmarks context usage
10 | * ``benchmark-plural.php`` - benchmarks plural evaluation
11 | * ``benchmark.php`` - benchmarks file parsing
12 | * ``benchmark-apcu.php`` - benchmarks file parsing with APCu cache enabled
13 |
14 | ## Performance measurements
15 |
16 | The performance improvements based on individual changes in the code:
17 |
18 | | Stage | Seconds |
19 | | -------------- | --------------- |
20 | | Original code | 4.7929680347443 |
21 | | Remove nocache | 4.6308250427246 |
22 | | Direct endian | 4.5883052349091 |
23 | | Remove attribs | 4.5297479629517 |
24 | | String reader | 1.8148958683014 |
25 | | No offset | 1.2436759471893 |
26 | | Less attribs | 1.1722540855408 |
27 | | Remove shift | 1.0970499515533 |
28 | | Magic order | 1.0868430137634 |
29 |
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # motranslator
2 |
3 | Translation API for PHP using Gettext MO files.
4 |
5 | [](https://github.com/phpmyadmin/motranslator/actions/workflows/tests.yml?query=branch%3Amaster)
6 | [](https://codecov.io/github/phpmyadmin/motranslator?branch=master)
7 | [](https://scrutinizer-ci.com/g/phpmyadmin/motranslator/?branch=master)
8 | [](https://packagist.org/packages/phpmyadmin/motranslator)
9 |
10 | ## Features
11 |
12 | * All strings are stored in memory for fast lookup
13 | * Fast loading of MO files
14 | * Low level API for reading MO files
15 | * Emulation of Gettext API
16 | * No use of `eval()` for plural equation
17 |
18 | ## Limitations
19 |
20 | * Default `InMemoryCache` not suitable for huge MO files which you don't want to store in memory
21 | * Input and output encoding has to match (preferably UTF-8)
22 |
23 | ## Installation
24 |
25 | Please use [Composer][1] to install:
26 |
27 | ```sh
28 | composer require phpmyadmin/motranslator
29 | ```
30 |
31 | ## Documentation
32 |
33 | The API documentation is available at .
34 |
35 | ## Object API usage
36 |
37 | ```php
38 | // Create loader object
39 | $loader = new PhpMyAdmin\MoTranslator\Loader();
40 |
41 | // Set locale
42 | $loader->setlocale('cs');
43 |
44 | // Set default text domain
45 | $loader->textdomain('domain');
46 |
47 | // Set path where to look for a domain
48 | $loader->bindtextdomain('domain', __DIR__ . '/data/locale/');
49 |
50 | // Get translator
51 | $translator = $loader->getTranslator();
52 |
53 | // Now you can use Translator API (see below)
54 | ```
55 |
56 | ## Low level API usage
57 |
58 | ```php
59 | // Directly load the mo file
60 | // You can use null to not load a file and the use a setter to set the translations
61 | $cache = new PhpMyAdmin\MoTranslator\Cache\InMemoryCache(new PhpMyAdmin\MoTranslator\MoParser('./path/to/file.mo'));
62 | $translator = new PhpMyAdmin\MoTranslator\Translator($cache);
63 |
64 | // Now you can use Translator API (see below)
65 | ```
66 |
67 | ## Translator API usage
68 |
69 | ```php
70 | // Translate string
71 | echo $translator->gettext('String');
72 |
73 | // Translate plural string
74 | echo $translator->ngettext('String', 'Plural string', $count);
75 |
76 | // Translate string with context
77 | echo $translator->pgettext('Context', 'String');
78 |
79 | // Translate plural string with context
80 | echo $translator->npgettext('Context', 'String', 'Plural string', $count);
81 |
82 | // Get the translations
83 | echo $translator->getTranslations();
84 |
85 | // All getters and setters below are more to be used if you are using a manual loading mode
86 | // Example: $translator = new PhpMyAdmin\MoTranslator\Translator(null);
87 |
88 | // Set a translation
89 | echo $translator->setTranslation('Test', 'Translation for "Test" key');
90 |
91 | // Set translations
92 | echo $translator->setTranslations([
93 | 'Test' => 'Translation for "Test" key',
94 | 'Test 2' => 'Translation for "Test 2" key',
95 | ]);
96 |
97 | // Use the translation
98 | echo $translator->gettext('Test 2'); // -> Translation for "Test 2" key
99 | ```
100 |
101 | ## Gettext compatibility usage
102 |
103 | ```php
104 | // Load compatibility layer
105 | PhpMyAdmin\MoTranslator\Loader::loadFunctions();
106 |
107 | // Configure
108 | _setlocale(LC_MESSAGES, 'cs');
109 | _textdomain('phpmyadmin');
110 | _bindtextdomain('phpmyadmin', __DIR__ . '/data/locale/');
111 | _bind_textdomain_codeset('phpmyadmin', 'UTF-8');
112 |
113 | // Use functions
114 | echo _gettext('Type');
115 | echo __('Type');
116 |
117 | // It also support other Gettext functions
118 | _dnpgettext($domain, $msgctxt, $msgid, $msgidPlural, $number);
119 | _dngettext($domain, $msgid, $msgidPlural, $number);
120 | _npgettext($msgctxt, $msgid, $msgidPlural, $number);
121 | _ngettext($msgid, $msgidPlural, $number);
122 | _dpgettext($domain, $msgctxt, $msgid);
123 | _dgettext($domain, $msgid);
124 | _pgettext($msgctxt, $msgid);
125 | ```
126 |
127 | ## Using APCu-backed cache
128 |
129 | If you have the [APCu][5] extension installed you can use it for storing the translation cache. The `.mo` file
130 | will then only be loaded once and all processes will share the same cache, reducing memory usage and resulting in
131 | performance comparable to the native `gettext` extension.
132 |
133 | If you are using `Loader`, pass it an `ApcuCacheFactory` _before_ getting the translator instance:
134 |
135 | ```php
136 | PhpMyAdmin\MoTranslator\Loader::setCacheFactory(
137 | new PhpMyAdmin\MoTranslator\Cache\ApcuCacheFactory()
138 | );
139 | $loader = new PhpMyAdmin\MoTranslator\Loader();
140 |
141 | // Proceed as before
142 | ```
143 |
144 | If you are using the low level API, instantiate the `ApcuCache` directly:
145 |
146 | ```php
147 | $cache = new PhpMyAdmin\MoTranslator\Cache\ApcuCache(
148 | new PhpMyAdmin\MoTranslator\MoParser('./path/to/file.mo'),
149 | 'de_DE', // the locale
150 | 'phpmyadmin' // the domain
151 | );
152 | $translator = new PhpMyAdmin\MoTranslator\Translator($cache);
153 |
154 | // Proceed as before
155 | ```
156 |
157 | By default, APCu will cache the translations until next server restart and prefix the cache entries with `mo_` to
158 | avoid clashes with other cache entries. You can control this behaviour by passing `$ttl` and `$prefix` arguments, either
159 | to the `ApcuCacheFactory` or when instantiating `ApcuCache`:
160 |
161 | ```php
162 | PhpMyAdmin\MoTranslator\Loader::setCacheFactory(
163 | new PhpMyAdmin\MoTranslator\Cache\ApcuCacheFactory(
164 | 3600, // cache for 1 hour
165 | true, // reload on cache miss
166 | 'custom_' // custom prefix for cache entries
167 | )
168 | );
169 | $loader = new PhpMyAdmin\MoTranslator\Loader();
170 |
171 | // or...
172 |
173 | $cache = new PhpMyAdmin\MoTranslator\Cache\ApcuCache(
174 | new PhpMyAdmin\MoTranslator\MoParser('./path/to/file.mo'),
175 | 'de_DE',
176 | 'phpmyadmin',
177 | 3600, // cache for 1 hour
178 | true, // reload on cache miss
179 | 'custom_' // custom prefix for cache entries
180 | );
181 | $translator = new PhpMyAdmin\MoTranslator\Translator($cache);
182 | ```
183 |
184 | If you receive updated translation files you can load them without restarting the server using the low-level API:
185 |
186 | ```php
187 | $parser = new PhpMyAdmin\MoTranslator\MoParser('./path/to/file.mo');
188 | $cache = new PhpMyAdmin\MoTranslator\Cache\ApcuCache($parser, 'de_DE', 'phpmyadmin');
189 | $parser->parseIntoCache($cache);
190 | ```
191 |
192 | You should ensure APCu has enough memory to store all your translations, along with any other entries you use it
193 | for. If an entry is evicted from cache, the `.mo` file will be re-parsed, impacting performance. See the
194 | `apc.shm_size` and `apc.shm_segments` [documentation][6] and monitor cache usage when first rolling out.
195 |
196 | If your `.mo` files are missing lots of translations, the first time a missing entry is requested the `.mo` file
197 | will be re-parsed. Again, this will impact performance until all the missing entries are hit once. You can turn off this
198 | behaviour by setting the `$reloadOnMiss` argument to `false`. If you do this it is _critical_ that APCu has enough
199 | memory, or users will see untranslated text when entries are evicted.
200 |
201 | ## History
202 |
203 | This library is based on [php-gettext][2]. It adds some performance
204 | improvements and ability to install using [Composer][1].
205 |
206 | ## Motivation
207 |
208 | Motivation for this library includes:
209 |
210 | * The [php-gettext][2] library is not maintained anymore
211 | * It doesn't work with recent PHP version (phpMyAdmin has patched version)
212 | * It relies on `eval()` function for plural equations what can have severe security implications, see [CVE-2016-6175][4]
213 | * It's not possible to install it using [Composer][1]
214 | * There was place for performance improvements in the library
215 |
216 | ### Why not to use native gettext in PHP?
217 |
218 | We've tried that, but it's not a viable solution:
219 |
220 | * You can not use locales not known to system, what is something you can not
221 | control from web application. This gets even more tricky with minimalist
222 | virtualisation containers.
223 | * Changing the MO file usually leads to PHP segmentation fault. It (or rather
224 | Gettext library) caches headers of MO file and if it's content is changed
225 | (for example new version is uploaded to server) it tries to access new data
226 | with old references. This is bug known for ages:
227 | https://bugs.php.net/bug.php?id=45943
228 |
229 | ### Why use Gettext and not JSON, YAML or whatever?
230 |
231 | We want translators to be able to use their favorite tools and we want us to be
232 | able to use wide range of tools available with Gettext as well such as
233 | [web based translation using Weblate][3]. Using custom format usually adds
234 | another barrier for translators and we want to make it easy for them to
235 | contribute.
236 |
237 | [1]:https://getcomposer.org/
238 | [2]:https://launchpad.net/php-gettext
239 | [3]:https://weblate.org/
240 | [4]: https://www.cve.org/CVERecord?id=CVE-2016-6175
241 | [5]:https://www.php.net/manual/en/book.apcu.php
242 | [6]:https://www.php.net/manual/en/apcu.configuration.php
243 |
--------------------------------------------------------------------------------
/benchmark-apcu.php:
--------------------------------------------------------------------------------
1 | './tests/data/big.mo',
13 | 'little' => './tests/data/little.mo',
14 | ];
15 |
16 | $start = microtime(true);
17 |
18 | for ($i = 0; $i < 2000; ++$i) {
19 | foreach ($files as $domain => $filename) {
20 | $translator = new Translator(new ApcuCache(new MoParser($filename), 'foo', $domain));
21 | $translator->gettext('Column');
22 | }
23 | }
24 |
25 | $end = microtime(true);
26 |
27 | $diff = $end - $start;
28 |
29 | echo 'Execution took ' . $diff . ' seconds' . "\n";
30 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phpmyadmin/motranslator",
3 | "description": "Translation API for PHP using Gettext MO files",
4 | "license": "GPL-2.0-or-later",
5 | "keywords": ["gettext", "mo", "translator", "i18n"],
6 | "homepage": "https://github.com/phpmyadmin/motranslator",
7 | "authors": [
8 | {
9 | "name": "The phpMyAdmin Team",
10 | "email": "developers@phpmyadmin.net",
11 | "homepage": "https://www.phpmyadmin.net/team/"
12 | }
13 | ],
14 | "support": {
15 | "issues": "https://github.com/phpmyadmin/motranslator/issues",
16 | "source": "https://github.com/phpmyadmin/motranslator"
17 | },
18 | "scripts": {
19 | "phpcbf": "@php phpcbf",
20 | "phpcs": "@php phpcs",
21 | "phpstan": "@php phpstan",
22 | "phpunit": "@php phpunit",
23 | "test": [
24 | "@phpcs",
25 | "@phpstan",
26 | "@phpunit"
27 | ]
28 | },
29 | "require": {
30 | "php": "^8.2",
31 | "symfony/expression-language": "^6.4 || ^7.0"
32 | },
33 | "require-dev": {
34 | "phpmyadmin/coding-standard": "^4.0",
35 | "phpstan/extension-installer": "^1.4",
36 | "phpstan/phpstan": "^2.1",
37 | "phpstan/phpstan-deprecation-rules": "^2.0",
38 | "phpstan/phpstan-phpunit": "^2.0",
39 | "phpstan/phpstan-strict-rules": "^2.0",
40 | "phpunit/phpunit": "^11.5 || ^12.0",
41 | "psalm/plugin-phpunit": "^0.19.2",
42 | "vimeo/psalm": "^6.8"
43 | },
44 | "suggest": {
45 | "ext-apcu": "Needed for APCu-backed translation cache"
46 | },
47 | "autoload": {
48 | "psr-4": {
49 | "PhpMyAdmin\\MoTranslator\\": "src"
50 | },
51 | "files": ["src/functions.php"]
52 | },
53 | "autoload-dev": {
54 | "psr-4": {
55 | "PhpMyAdmin\\MoTranslator\\Tests\\": "tests"
56 | }
57 | },
58 | "config": {
59 | "allow-plugins": {
60 | "dealerdirect/phpcodesniffer-composer-installer": true,
61 | "phpstan/extension-installer": true
62 | },
63 | "discard-changes": true,
64 | "sort-packages": true
65 | },
66 | "extra": {
67 | "branch-alias": {
68 | "dev-master": "6.0-dev"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | src/
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./tests
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/psalm-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | pluralCount]]>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Cache/ApcuCache.php:
--------------------------------------------------------------------------------
1 | ensureTranslationsLoaded();
40 | }
41 |
42 | public function get(string $msgid): string
43 | {
44 | $msgstr = apcu_fetch($this->getKey($msgid), $success);
45 | if ($success && is_string($msgstr)) {
46 | return $msgstr;
47 | }
48 |
49 | if (! $this->reloadOnMiss) {
50 | return $msgid;
51 | }
52 |
53 | return $this->reloadOnMiss($msgid);
54 | }
55 |
56 | private function reloadOnMiss(string $msgid): string
57 | {
58 | // store original if translation is not present
59 | $cached = apcu_entry($this->getKey($msgid), static function () use ($msgid) {
60 | return $msgid;
61 | }, $this->ttl);
62 | // if another process has updated cache, return early
63 | if ($cached !== $msgid && is_string($cached)) {
64 | return $cached;
65 | }
66 |
67 | // reload .mo file, in case entry has been evicted
68 | $this->parser->parseIntoCache($this);
69 |
70 | $msgstr = apcu_fetch($this->getKey($msgid), $success);
71 |
72 | return $success && is_string($msgstr) ? $msgstr : $msgid;
73 | }
74 |
75 | public function set(string $msgid, string $msgstr): void
76 | {
77 | apcu_store($this->getKey($msgid), $msgstr, $this->ttl);
78 | }
79 |
80 | public function has(string $msgid): bool
81 | {
82 | return apcu_exists($this->getKey($msgid));
83 | }
84 |
85 | /** @inheritDoc */
86 | public function setAll(array $translations): void
87 | {
88 | $keys = array_map(function (string $msgid): string {
89 | return $this->getKey($msgid);
90 | }, array_keys($translations));
91 | $translations = array_combine($keys, $translations);
92 | assert(is_array($translations));
93 |
94 | apcu_store($translations, null, $this->ttl);
95 | }
96 |
97 | private function getKey(string $msgid): string
98 | {
99 | return $this->prefix . $this->locale . '.' . $this->domain . '.' . $msgid;
100 | }
101 |
102 | private function ensureTranslationsLoaded(): void
103 | {
104 | // Try to prevent cache slam if multiple processes are trying to load translations. There is still a race
105 | // between the exists check and creating the entry, but at least it's small
106 | $key = $this->getKey(self::LOADED_KEY);
107 | $loaded = apcu_exists($key) || apcu_entry($key, static function (): int {
108 | return 0;
109 | }, $this->ttl);
110 | if ($loaded) {
111 | return;
112 | }
113 |
114 | $this->parser->parseIntoCache($this);
115 | apcu_store($this->getKey(self::LOADED_KEY), 1, $this->ttl);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Cache/ApcuCacheFactory.php:
--------------------------------------------------------------------------------
1 | ttl, $this->reloadOnMiss, $this->prefix);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Cache/CacheFactoryInterface.php:
--------------------------------------------------------------------------------
1 | $msgstr` entries
26 | *
27 | * This will overwrite existing values for `$msgid`, but is not guaranteed to clear cache of existing entries
28 | * not present in `$translations`.
29 | *
30 | * @param array $translations
31 | */
32 | public function setAll(array $translations): void;
33 | }
34 |
--------------------------------------------------------------------------------
/src/Cache/GetAllInterface.php:
--------------------------------------------------------------------------------
1 | */
10 | public function getAll(): array;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Cache/InMemoryCache.php:
--------------------------------------------------------------------------------
1 | */
14 | private array $cache = [];
15 |
16 | public function __construct(MoParser $parser)
17 | {
18 | $parser->parseIntoCache($this);
19 | }
20 |
21 | public function get(string $msgid): string
22 | {
23 | return array_key_exists($msgid, $this->cache) ? $this->cache[$msgid] : $msgid;
24 | }
25 |
26 | public function set(string $msgid, string $msgstr): void
27 | {
28 | $this->cache[$msgid] = $msgstr;
29 | }
30 |
31 | public function has(string $msgid): bool
32 | {
33 | return array_key_exists($msgid, $this->cache);
34 | }
35 |
36 | /** @inheritDoc */
37 | public function setAll(array $translations): void
38 | {
39 | $this->cache = $translations;
40 | }
41 |
42 | /** @inheritDoc */
43 | public function getAll(): array
44 | {
45 | return $this->cache;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/CacheException.php:
--------------------------------------------------------------------------------
1 |
7 | Copyright (c) 2009 Danilo Segan
8 | Copyright (c) 2016 Michal Čihař
9 |
10 | This file is part of MoTranslator.
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License along
23 | with this program; if not, write to the Free Software Foundation, Inc.,
24 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 | */
26 |
27 | namespace PhpMyAdmin\MoTranslator;
28 |
29 | use PhpMyAdmin\MoTranslator\Cache\CacheFactoryInterface;
30 | use PhpMyAdmin\MoTranslator\Cache\InMemoryCache;
31 |
32 | use function file_exists;
33 | use function getenv;
34 | use function in_array;
35 | use function preg_match;
36 | use function sprintf;
37 |
38 | class Loader
39 | {
40 | /**
41 | * Loader instance.
42 | *
43 | * @static
44 | */
45 | private static Loader|null $instance = null;
46 |
47 | /**
48 | * Factory to return a factory responsible for returning a `CacheInterface`
49 | *
50 | * @static
51 | */
52 | private static CacheFactoryInterface|null $cacheFactory = null;
53 |
54 | /**
55 | * Default gettext domain to use.
56 | */
57 | private string $defaultDomain = '';
58 |
59 | /**
60 | * Configured locale.
61 | */
62 | private string $locale = '';
63 |
64 | /**
65 | * Loaded domains.
66 | *
67 | * @var array>
68 | */
69 | private array $domains = [];
70 |
71 | /**
72 | * Bound paths for domains.
73 | *
74 | * @var array
75 | */
76 | private array $paths = ['' => './'];
77 |
78 | /**
79 | * Returns the singleton Loader object.
80 | *
81 | * @return Loader object
82 | */
83 | public static function getInstance(): Loader
84 | {
85 | if (self::$instance === null) {
86 | self::$instance = new self();
87 | }
88 |
89 | return self::$instance;
90 | }
91 |
92 | /**
93 | * Loads global localization functions.
94 | */
95 | public static function loadFunctions(): void
96 | {
97 | require_once __DIR__ . '/functions.php';
98 | }
99 |
100 | /**
101 | * Figure out all possible locale names and start with the most
102 | * specific ones. I.e. for sr_CS.UTF-8@latin, look through all of
103 | * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr.
104 | *
105 | * @param string $locale Locale code
106 | *
107 | * @return string[] list of locales to try for any POSIX-style locale specification
108 | * @psalm-return list
109 | */
110 | public static function listLocales(string $locale): array
111 | {
112 | $localeNames = [];
113 |
114 | if ($locale !== '') {
115 | if (
116 | preg_match(
117 | '/^(?P[a-z]{2,3})' // language code
118 | . '(?:_(?P[A-Z]{2}))?' // country code
119 | . '(?:\\.(?P[-A-Za-z0-9_]+))?' // charset
120 | . '(?:@(?P[-A-Za-z0-9_]+))?$/', // @ modifier
121 | $locale,
122 | $matches,
123 | ) === 1
124 | ) {
125 | $lang = $matches['lang'] ?? '';
126 | $country = $matches['country'] ?? '';
127 | $charset = $matches['charset'] ?? '';
128 | $modifier = $matches['modifier'] ?? '';
129 |
130 | if ($modifier !== '') {
131 | if ($country !== '') {
132 | if ($charset !== '') {
133 | $localeNames[] = sprintf('%s_%s.%s@%s', $lang, $country, $charset, $modifier);
134 | }
135 |
136 | $localeNames[] = sprintf('%s_%s@%s', $lang, $country, $modifier);
137 | } elseif ($charset !== '') {
138 | $localeNames[] = sprintf('%s.%s@%s', $lang, $charset, $modifier);
139 | }
140 |
141 | $localeNames[] = sprintf('%s@%s', $lang, $modifier);
142 | }
143 |
144 | if ($country !== '') {
145 | if ($charset !== '') {
146 | $localeNames[] = sprintf('%s_%s.%s', $lang, $country, $charset);
147 | }
148 |
149 | $localeNames[] = sprintf('%s_%s', $lang, $country);
150 | } elseif ($charset !== '') {
151 | $localeNames[] = sprintf('%s.%s', $lang, $charset);
152 | }
153 |
154 | if ($lang !== '') {
155 | $localeNames[] = $lang;
156 | }
157 | }
158 |
159 | // If the locale name doesn't match POSIX style, just include it as-is.
160 | if (! in_array($locale, $localeNames, true)) {
161 | $localeNames[] = $locale;
162 | }
163 | }
164 |
165 | return $localeNames;
166 | }
167 |
168 | /**
169 | * Sets factory responsible for composing a `CacheInterface`
170 | */
171 | public static function setCacheFactory(CacheFactoryInterface|null $cacheFactory): void
172 | {
173 | self::$cacheFactory = $cacheFactory;
174 | }
175 |
176 | /**
177 | * Returns Translator object for domain or for default domain.
178 | *
179 | * @param string $domain Translation domain
180 | */
181 | public function getTranslator(string $domain = ''): Translator
182 | {
183 | if ($domain === '') {
184 | $domain = $this->defaultDomain;
185 | }
186 |
187 | $this->domains[$this->locale] ??= [];
188 |
189 | if (! isset($this->domains[$this->locale][$domain])) {
190 | $base = $this->paths[$domain] ?? './';
191 |
192 | $localeNames = self::listLocales($this->locale);
193 |
194 | $filename = '';
195 | foreach ($localeNames as $locale) {
196 | $filename = $base . '/' . $locale . '/LC_MESSAGES/' . $domain . '.mo';
197 | if (file_exists($filename)) {
198 | break;
199 | }
200 | }
201 |
202 | // We don't care about invalid path, we will get fallback
203 | // translator here
204 | $moParser = new MoParser($filename);
205 | if (self::$cacheFactory instanceof CacheFactoryInterface) {
206 | $cache = self::$cacheFactory->getInstance($moParser, $this->locale, $domain);
207 | } else {
208 | $cache = new InMemoryCache($moParser);
209 | }
210 |
211 | $this->domains[$this->locale][$domain] = new Translator($cache);
212 | }
213 |
214 | return $this->domains[$this->locale][$domain];
215 | }
216 |
217 | /**
218 | * Sets the path for a domain.
219 | *
220 | * @param string $domain Domain name
221 | * @param string $path Path where to find locales
222 | */
223 | public function bindtextdomain(string $domain, string $path): void
224 | {
225 | $this->paths[$domain] = $path;
226 | }
227 |
228 | /**
229 | * Sets the default domain.
230 | *
231 | * @param string $domain Domain name
232 | */
233 | public function textdomain(string $domain): void
234 | {
235 | $this->defaultDomain = $domain;
236 | }
237 |
238 | /**
239 | * Sets a requested locale.
240 | *
241 | * @param string $locale Locale name
242 | *
243 | * @return string Set or current locale
244 | */
245 | public function setlocale(string $locale): string
246 | {
247 | if (! empty($locale)) {
248 | $this->locale = $locale;
249 | }
250 |
251 | return $this->locale;
252 | }
253 |
254 | /**
255 | * Detects currently configured locale.
256 | *
257 | * It checks:
258 | *
259 | * - global lang variable
260 | * - environment for LC_ALL, LC_MESSAGES and LANG
261 | *
262 | * @return string with locale name
263 | */
264 | public function detectlocale(): string
265 | {
266 | if (isset($GLOBALS['lang'])) {
267 | return $GLOBALS['lang'];
268 | }
269 |
270 | $locale = getenv('LC_ALL');
271 | if ($locale !== false) {
272 | return $locale;
273 | }
274 |
275 | $locale = getenv('LC_MESSAGES');
276 | if ($locale !== false) {
277 | return $locale;
278 | }
279 |
280 | $locale = getenv('LANG');
281 | if ($locale !== false) {
282 | return $locale;
283 | }
284 |
285 | return 'en';
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/src/MoParser.php:
--------------------------------------------------------------------------------
1 | filename === null) {
58 | return;
59 | }
60 |
61 | if (! is_readable($this->filename)) {
62 | $this->error = self::ERROR_DOES_NOT_EXIST;
63 |
64 | return;
65 | }
66 |
67 | $stream = new StringReader($this->filename);
68 |
69 | try {
70 | $magic = $stream->read(0, 4);
71 | if ($magic === self::MAGIC_LE) {
72 | $unpack = 'V';
73 | } elseif ($magic === self::MAGIC_BE) {
74 | $unpack = 'N';
75 | } else {
76 | $this->error = self::ERROR_BAD_MAGIC;
77 |
78 | return;
79 | }
80 |
81 | /* Parse header */
82 | $total = $stream->readint($unpack, 8);
83 | $originals = $stream->readint($unpack, 12);
84 | $translations = $stream->readint($unpack, 16);
85 |
86 | /* get original and translations tables */
87 | $totalTimesTwo = (int) ($total * 2);// Fix for issue #36 on ARM
88 | $tableOriginals = $stream->readintarray($unpack, $originals, $totalTimesTwo);
89 | $tableTranslations = $stream->readintarray($unpack, $translations, $totalTimesTwo);
90 |
91 | /* read all strings to the cache */
92 | for ($i = 0; $i < $total; ++$i) {
93 | $iTimesTwo = $i * 2;
94 | $iPlusOne = $iTimesTwo + 1;
95 | $iPlusTwo = $iTimesTwo + 2;
96 | $original = $stream->read($tableOriginals[$iPlusTwo], $tableOriginals[$iPlusOne]);
97 | $translation = $stream->read($tableTranslations[$iPlusTwo], $tableTranslations[$iPlusOne]);
98 | $cache->set($original, $translation);
99 | }
100 | } catch (ReaderException) {
101 | $this->error = self::ERROR_READING;
102 |
103 | return;
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/ReaderException.php:
--------------------------------------------------------------------------------
1 | .
7 | Copyright (c) 2016 Michal Čihař
8 |
9 | This file is part of MoTranslator.
10 |
11 | This program is free software; you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation; either version 2 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License along
22 | with this program; if not, write to the Free Software Foundation, Inc.,
23 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 | */
25 |
26 | namespace PhpMyAdmin\MoTranslator;
27 |
28 | use Exception;
29 |
30 | /**
31 | * Exception thrown when file can not be read.
32 | */
33 | class ReaderException extends Exception
34 | {
35 | }
36 |
--------------------------------------------------------------------------------
/src/StringReader.php:
--------------------------------------------------------------------------------
1 | .
7 | Copyright (c) 2016 Michal Čihař
8 |
9 | This file is part of MoTranslator.
10 |
11 | This program is free software; you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation; either version 2 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License along
22 | with this program; if not, write to the Free Software Foundation, Inc.,
23 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 | */
25 |
26 | namespace PhpMyAdmin\MoTranslator;
27 |
28 | use function file_get_contents;
29 | use function strlen;
30 | use function substr;
31 | use function unpack;
32 |
33 | use const PHP_INT_MAX;
34 |
35 | /**
36 | * Simple wrapper around string buffer for
37 | * random access and values parsing.
38 | */
39 | class StringReader
40 | {
41 | private string $string;
42 | private int $length;
43 |
44 | /** @param string $filename Name of file to load */
45 | public function __construct(string $filename)
46 | {
47 | $this->string = (string) file_get_contents($filename);
48 | $this->length = strlen($this->string);
49 | }
50 |
51 | /**
52 | * Read number of bytes from given offset.
53 | *
54 | * @param int $pos Offset
55 | * @param int $bytes Number of bytes to read
56 | */
57 | public function read(int $pos, int $bytes): string
58 | {
59 | if ($pos + $bytes > $this->length) {
60 | throw new ReaderException('Not enough bytes!');
61 | }
62 |
63 | return substr($this->string, $pos, $bytes);
64 | }
65 |
66 | /**
67 | * Reads a 32bit integer from the stream.
68 | *
69 | * @param string $unpack Unpack string
70 | * @param int $pos Position
71 | *
72 | * @return int Integer from the stream
73 | */
74 | public function readint(string $unpack, int $pos): int
75 | {
76 | $data = unpack($unpack, $this->read($pos, 4));
77 | if ($data === false) {
78 | return PHP_INT_MAX;
79 | }
80 |
81 | $result = $data[1];
82 |
83 | /* We're reading unsigned int, but PHP will happily
84 | * give us negative number on 32-bit platforms.
85 | *
86 | * See also documentation:
87 | * https://secure.php.net/manual/en/function.unpack.php#refsect1-function.unpack-notes
88 | */
89 | return $result < 0 ? PHP_INT_MAX : $result;
90 | }
91 |
92 | /**
93 | * Reads an array of integers from the stream.
94 | *
95 | * @param string $unpack Unpack string
96 | * @param int $pos Position
97 | * @param int $count How many elements should be read
98 | *
99 | * @return int[] Array of Integers
100 | */
101 | public function readintarray(string $unpack, int $pos, int $count): array
102 | {
103 | $data = unpack($unpack . $count, $this->read($pos, 4 * $count));
104 | if ($data === false) {
105 | return [];
106 | }
107 |
108 | return $data;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Translator.php:
--------------------------------------------------------------------------------
1 | .
7 | Copyright (c) 2005 Nico Kaiser
8 | Copyright (c) 2016 Michal Čihař
9 |
10 | This file is part of MoTranslator.
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License along
23 | with this program; if not, write to the Free Software Foundation, Inc.,
24 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 | */
26 |
27 | namespace PhpMyAdmin\MoTranslator;
28 |
29 | use PhpMyAdmin\MoTranslator\Cache\CacheInterface;
30 | use PhpMyAdmin\MoTranslator\Cache\GetAllInterface;
31 | use PhpMyAdmin\MoTranslator\Cache\InMemoryCache;
32 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
33 | use Throwable;
34 |
35 | use function array_key_exists;
36 | use function count;
37 | use function explode;
38 | use function is_numeric;
39 | use function ltrim;
40 | use function preg_replace;
41 | use function rtrim;
42 | use function sprintf;
43 | use function str_contains;
44 | use function str_starts_with;
45 | use function stripos;
46 | use function strtolower;
47 | use function substr;
48 | use function trim;
49 |
50 | /**
51 | * Provides a simple gettext replacement that works independently from
52 | * the system's gettext abilities.
53 | * It can read MO files and use them for translating strings.
54 | *
55 | * It caches ll strings and translations to speed up the string lookup.
56 | */
57 | class Translator
58 | {
59 | /**
60 | * None error.
61 | */
62 | public const ERROR_NONE = 0;
63 |
64 | /**
65 | * File does not exist.
66 | */
67 | public const ERROR_DOES_NOT_EXIST = 1;
68 |
69 | /**
70 | * File has bad magic number.
71 | */
72 | public const ERROR_BAD_MAGIC = 2;
73 |
74 | /**
75 | * Error while reading file, probably too short.
76 | */
77 | public const ERROR_READING = 3;
78 |
79 | /**
80 | * Big endian mo file magic bytes.
81 | */
82 | public const MAGIC_BE = "\x95\x04\x12\xde";
83 |
84 | /**
85 | * Little endian mo file magic bytes.
86 | */
87 | public const MAGIC_LE = "\xde\x12\x04\x95";
88 |
89 | /**
90 | * Parse error code (0 if no error).
91 | */
92 | public int $error = self::ERROR_NONE;
93 |
94 | /**
95 | * Cache header field for plural forms.
96 | */
97 | private string|null $pluralEquation = null;
98 |
99 | /**
100 | * Evaluator for plurals
101 | */
102 | private ExpressionLanguage|null $pluralExpression = null;
103 |
104 | /**
105 | * number of plurals
106 | */
107 | private int|null $pluralCount = null;
108 |
109 | private CacheInterface $cache;
110 |
111 | /** @param CacheInterface|string|null $cache Mo file to load (null for no file) or a CacheInterface implementation */
112 | public function __construct(CacheInterface|string|null $cache)
113 | {
114 | if (! $cache instanceof CacheInterface) {
115 | $cache = new InMemoryCache(new MoParser($cache));
116 | }
117 |
118 | $this->cache = $cache;
119 | }
120 |
121 | /**
122 | * Translates a string.
123 | *
124 | * @param string $msgid String to be translated
125 | *
126 | * @return string translated string (or original, if not found)
127 | */
128 | public function gettext(string $msgid): string
129 | {
130 | return $this->cache->get($msgid);
131 | }
132 |
133 | /**
134 | * Check if a string is translated.
135 | *
136 | * @param string $msgid String to be checked
137 | */
138 | public function exists(string $msgid): bool
139 | {
140 | return $this->cache->has($msgid);
141 | }
142 |
143 | /**
144 | * Sanitize plural form expression for use in ExpressionLanguage.
145 | *
146 | * @param string $expr Expression to sanitize
147 | *
148 | * @return string sanitized plural form expression
149 | */
150 | public static function sanitizePluralExpression(string $expr): string
151 | {
152 | // Parse equation
153 | $expr = explode(';', $expr);
154 | $expr = count($expr) >= 2 ? $expr[1] : $expr[0];
155 |
156 | $expr = trim(strtolower($expr));
157 | // Strip plural prefix
158 | if (str_starts_with($expr, 'plural')) {
159 | $expr = ltrim(substr($expr, 6));
160 | }
161 |
162 | // Strip equals
163 | if (str_starts_with($expr, '=')) {
164 | $expr = ltrim(substr($expr, 1));
165 | }
166 |
167 | // Cleanup from unwanted chars
168 | $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&| ]@', '', $expr);
169 |
170 | return (string) $expr;
171 | }
172 |
173 | /**
174 | * Extracts number of plurals from plurals form expression.
175 | *
176 | * @param string $expr Expression to process
177 | *
178 | * @return int Total number of plurals
179 | */
180 | public static function extractPluralCount(string $expr): int
181 | {
182 | $parts = explode(';', $expr, 2);
183 | $nplurals = explode('=', trim($parts[0]), 2);
184 | if (strtolower(rtrim($nplurals[0])) !== 'nplurals') {
185 | return 1;
186 | }
187 |
188 | if (count($nplurals) === 1) {
189 | return 1;
190 | }
191 |
192 | return (int) $nplurals[1];
193 | }
194 |
195 | /**
196 | * Parse full PO header and extract only plural forms line.
197 | *
198 | * @param string $header Gettext header
199 | *
200 | * @return string verbatim plural form header field
201 | */
202 | public static function extractPluralsForms(string $header): string
203 | {
204 | $headers = explode("\n", $header);
205 | $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
206 | foreach ($headers as $header) {
207 | if (stripos($header, 'Plural-Forms:') !== 0) {
208 | continue;
209 | }
210 |
211 | $expr = substr($header, 13);
212 | }
213 |
214 | return $expr;
215 | }
216 |
217 | /**
218 | * Get possible plural forms from MO header.
219 | *
220 | * @return string plural form header
221 | */
222 | private function getPluralForms(): string
223 | {
224 | // lets assume message number 0 is header
225 | // this is true, right?
226 |
227 | // cache header field for plural forms
228 | if ($this->pluralEquation === null) {
229 | $header = $this->cache->get('');
230 |
231 | $expr = self::extractPluralsForms($header);
232 | $this->pluralEquation = self::sanitizePluralExpression($expr);
233 | $this->pluralCount = self::extractPluralCount($expr);
234 | }
235 |
236 | return $this->pluralEquation;
237 | }
238 |
239 | /**
240 | * Detects which plural form to take.
241 | *
242 | * @param int $n count of objects
243 | *
244 | * @return int array index of the right plural form
245 | */
246 | private function selectString(int $n): int
247 | {
248 | if ($this->pluralExpression === null) {
249 | $this->pluralExpression = new ExpressionLanguage();
250 | }
251 |
252 | try {
253 | $evaluatedPlural = $this->pluralExpression->evaluate($this->getPluralForms(), ['n' => $n]);
254 | $plural = is_numeric($evaluatedPlural) ? (int) $evaluatedPlural : 0;
255 | } catch (Throwable) {
256 | $plural = 0;
257 | }
258 |
259 | if ($plural >= $this->pluralCount) {
260 | $plural = $this->pluralCount - 1;
261 | }
262 |
263 | return $plural;
264 | }
265 |
266 | /**
267 | * Plural version of gettext.
268 | *
269 | * @param string $msgid Single form
270 | * @param string $msgidPlural Plural form
271 | * @param int $number Number of objects
272 | *
273 | * @return string translated plural form
274 | */
275 | public function ngettext(string $msgid, string $msgidPlural, int $number): string
276 | {
277 | // this should contains all strings separated by NULLs
278 | $key = $msgid . "\u{0}" . $msgidPlural;
279 | if (! $this->cache->has($key)) {
280 | return $number !== 1 ? $msgidPlural : $msgid;
281 | }
282 |
283 | $result = $this->cache->get($key);
284 |
285 | // find out the appropriate form
286 | $select = $this->selectString($number);
287 |
288 | $list = explode("\u{0}", $result);
289 |
290 | if (array_key_exists($select, $list)) {
291 | return $list[$select];
292 | }
293 |
294 | return $list[0];
295 | }
296 |
297 | /**
298 | * Translate with context.
299 | *
300 | * @param string $msgctxt Context
301 | * @param string $msgid String to be translated
302 | *
303 | * @return string translated plural form
304 | */
305 | public function pgettext(string $msgctxt, string $msgid): string
306 | {
307 | $key = $msgctxt . "\u{4}" . $msgid;
308 | $ret = $this->gettext($key);
309 | if ($ret === $key) {
310 | return $msgid;
311 | }
312 |
313 | return $ret;
314 | }
315 |
316 | /**
317 | * Plural version of pgettext.
318 | *
319 | * @param string $msgctxt Context
320 | * @param string $msgid Single form
321 | * @param string $msgidPlural Plural form
322 | * @param int $number Number of objects
323 | *
324 | * @return string translated plural form
325 | */
326 | public function npgettext(string $msgctxt, string $msgid, string $msgidPlural, int $number): string
327 | {
328 | $key = $msgctxt . "\u{4}" . $msgid;
329 | $ret = $this->ngettext($key, $msgidPlural, $number);
330 | if (str_contains($ret, "\u{4}")) {
331 | return $msgid;
332 | }
333 |
334 | return $ret;
335 | }
336 |
337 | /**
338 | * Set translation in place
339 | *
340 | * @param string $msgid String to be set
341 | * @param string $msgstr Translation
342 | */
343 | public function setTranslation(string $msgid, string $msgstr): void
344 | {
345 | $this->cache->set($msgid, $msgstr);
346 | }
347 |
348 | /**
349 | * Set the translations
350 | *
351 | * @param array $translations The translations "key => value" array
352 | */
353 | public function setTranslations(array $translations): void
354 | {
355 | $this->cache->setAll($translations);
356 | }
357 |
358 | /**
359 | * Get the translations
360 | *
361 | * @return array The translations "key => value" array
362 | */
363 | public function getTranslations(): array
364 | {
365 | if ($this->cache instanceof GetAllInterface) {
366 | return $this->cache->getAll();
367 | }
368 |
369 | throw new CacheException(sprintf(
370 | "Cache '%s' does not support getting translations",
371 | $this->cache::class,
372 | ));
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 |
7 | Copyright (c) 2009 Danilo Segan
8 | Copyright (c) 2016 Michal Čihař
9 |
10 | This file is part of MoTranslator.
11 |
12 | This program is free software; you can redistribute it and/or modify
13 | it under the terms of the GNU General Public License as published by
14 | the Free Software Foundation; either version 2 of the License, or
15 | (at your option) any later version.
16 |
17 | This program is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU General Public License for more details.
21 |
22 | You should have received a copy of the GNU General Public License along
23 | with this program; if not, write to the Free Software Foundation, Inc.,
24 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 | */
26 |
27 | use PhpMyAdmin\MoTranslator\Loader;
28 |
29 | // phpcs:disable Squiz.Functions.GlobalFunction
30 |
31 | if (! function_exists('_setlocale')) {
32 | /**
33 | * Sets a requested locale.
34 | *
35 | * @param int $category Locale category, ignored
36 | * @param string $locale Locale name
37 | *
38 | * @return string Set or current locale
39 | */
40 | function _setlocale(int $category, string $locale): string
41 | {
42 | return Loader::getInstance()->setlocale($locale);
43 | }
44 | }
45 |
46 | if (! function_exists('_bindtextdomain')) {
47 | /**
48 | * Sets the path for a domain.
49 | *
50 | * @param string $domain Domain name
51 | * @param string $path Path where to find locales
52 | */
53 | function _bindtextdomain(string $domain, string $path): void
54 | {
55 | Loader::getInstance()->bindtextdomain($domain, $path);
56 | }
57 | }
58 |
59 | if (! function_exists('_bind_textdomain_codeset')) {
60 | /**
61 | * Dummy compatibility function, MoTranslator assumes
62 | * everything is using same character set on input and
63 | * output.
64 | *
65 | * Generally it is wise to output in UTF-8 and have
66 | * mo files in UTF-8.
67 | *
68 | * @param string $domain Domain where to set character set
69 | * @param string $codeset Character set to set
70 | */
71 | function _bind_textdomain_codeset(string $domain, string $codeset): void
72 | {
73 | }
74 | }
75 |
76 | if (! function_exists('_textdomain')) {
77 | /**
78 | * Sets the default domain.
79 | *
80 | * @param string $domain Domain name
81 | */
82 | function _textdomain(string $domain): void
83 | {
84 | Loader::getInstance()->textdomain($domain);
85 | }
86 | }
87 |
88 | if (! function_exists('_gettext')) {
89 | /**
90 | * Translates a string.
91 | *
92 | * @param string $msgid String to be translated
93 | *
94 | * @return string translated string (or original, if not found)
95 | */
96 | function _gettext(string $msgid): string
97 | {
98 | return Loader::getInstance()->getTranslator()->gettext($msgid);
99 | }
100 | }
101 |
102 | if (! function_exists('__')) {
103 | /**
104 | * Translates a string, alias for _gettext.
105 | *
106 | * @param string $msgid String to be translated
107 | *
108 | * @return string translated string (or original, if not found)
109 | */
110 | function __(string $msgid): string
111 | {
112 | return Loader::getInstance()->getTranslator()->gettext($msgid);
113 | }
114 | }
115 |
116 | if (! function_exists('_ngettext')) {
117 | /**
118 | * Plural version of gettext.
119 | *
120 | * @param string $msgid Single form
121 | * @param string $msgidPlural Plural form
122 | * @param int $number Number of objects
123 | *
124 | * @return string translated plural form
125 | */
126 | function _ngettext(string $msgid, string $msgidPlural, int $number): string
127 | {
128 | return Loader::getInstance()->getTranslator()->ngettext($msgid, $msgidPlural, $number);
129 | }
130 | }
131 |
132 | if (! function_exists('_pgettext')) {
133 | /**
134 | * Translate with context.
135 | *
136 | * @param string $msgctxt Context
137 | * @param string $msgid String to be translated
138 | *
139 | * @return string translated plural form
140 | */
141 | function _pgettext(string $msgctxt, string $msgid): string
142 | {
143 | return Loader::getInstance()->getTranslator()->pgettext($msgctxt, $msgid);
144 | }
145 | }
146 |
147 | if (! function_exists('_npgettext')) {
148 | /**
149 | * Plural version of pgettext.
150 | *
151 | * @param string $msgctxt Context
152 | * @param string $msgid Single form
153 | * @param string $msgidPlural Plural form
154 | * @param int $number Number of objects
155 | *
156 | * @return string translated plural form
157 | */
158 | function _npgettext(string $msgctxt, string $msgid, string $msgidPlural, int $number): string
159 | {
160 | return Loader::getInstance()->getTranslator()->npgettext($msgctxt, $msgid, $msgidPlural, $number);
161 | }
162 | }
163 |
164 | if (! function_exists('_dgettext')) {
165 | /**
166 | * Translates a string.
167 | *
168 | * @param string $domain Domain to use
169 | * @param string $msgid String to be translated
170 | *
171 | * @return string translated string (or original, if not found)
172 | */
173 | function _dgettext(string $domain, string $msgid): string
174 | {
175 | return Loader::getInstance()->getTranslator($domain)->gettext($msgid);
176 | }
177 | }
178 |
179 | if (! function_exists('_dngettext')) {
180 | /**
181 | * Plural version of gettext.
182 | *
183 | * @param string $domain Domain to use
184 | * @param string $msgid Single form
185 | * @param string $msgidPlural Plural form
186 | * @param int $number Number of objects
187 | *
188 | * @return string translated plural form
189 | */
190 | function _dngettext(string $domain, string $msgid, string $msgidPlural, int $number): string
191 | {
192 | return Loader::getInstance()->getTranslator($domain)->ngettext($msgid, $msgidPlural, $number);
193 | }
194 | }
195 |
196 | if (! function_exists('_dpgettext')) {
197 | /**
198 | * Translate with context.
199 | *
200 | * @param string $domain Domain to use
201 | * @param string $msgctxt Context
202 | * @param string $msgid String to be translated
203 | *
204 | * @return string translated plural form
205 | */
206 | function _dpgettext(string $domain, string $msgctxt, string $msgid): string
207 | {
208 | return Loader::getInstance()->getTranslator($domain)->pgettext($msgctxt, $msgid);
209 | }
210 | }
211 |
212 | if (! function_exists('_dnpgettext')) {
213 | /**
214 | * Plural version of pgettext.
215 | *
216 | * @param string $domain Domain to use
217 | * @param string $msgctxt Context
218 | * @param string $msgid Single form
219 | * @param string $msgidPlural Plural form
220 | * @param int $number Number of objects
221 | *
222 | * @return string translated plural form
223 | */
224 | function _dnpgettext(string $domain, string $msgctxt, string $msgid, string $msgidPlural, int $number): string
225 | {
226 | return Loader::getInstance()->getTranslator($domain)->npgettext($msgctxt, $msgid, $msgidPlural, $number);
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/tests/Cache/ApcuCacheFactoryTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('APCu extension is not installed and enabled for CLI');
32 | }
33 |
34 | protected function tearDown(): void
35 | {
36 | parent::tearDown();
37 |
38 | apcu_clear_cache();
39 | }
40 |
41 | public function testGetInstanceReturnApcuCache(): void
42 | {
43 | $factory = new ApcuCacheFactory();
44 | $instance = $factory->getInstance(new MoParser(null), 'foo', 'bar');
45 | self::assertInstanceOf(ApcuCache::class, $instance);
46 | }
47 |
48 | public function testConstructorSetsTtl(): void
49 | {
50 | $locale = 'foo';
51 | $domain = 'bar';
52 | $msgid = 'Column';
53 | $ttl = 1;
54 |
55 | $factory = new ApcuCacheFactory($ttl);
56 | $parser = new MoParser(__DIR__ . '/../data/little.mo');
57 | $factory->getInstance($parser, $locale, $domain);
58 | sleep($ttl * 2);
59 |
60 | apcu_fetch('mo_' . $locale . '.' . $domain . '.' . $msgid, $success);
61 | self::assertFalse($success);
62 | }
63 |
64 | public function testConstructorSetsReloadOnMiss(): void
65 | {
66 | $expected = 'Column';
67 | $locale = 'foo';
68 | $domain = 'bar';
69 | $msgid = 'Column';
70 |
71 | $factory = new ApcuCacheFactory(0, false);
72 | $parser = new MoParser(__DIR__ . '/../data/little.mo');
73 |
74 | $instance = $factory->getInstance($parser, $locale, $domain);
75 |
76 | apcu_delete('mo_' . $locale . '.' . $domain . '.' . $msgid);
77 | $actual = $instance->get($msgid);
78 | self::assertSame($expected, $actual);
79 | }
80 |
81 | public function testConstructorSetsPrefix(): void
82 | {
83 | $expected = 'Pole';
84 | $locale = 'foo';
85 | $domain = 'bar';
86 | $msgid = 'Column';
87 | $prefix = 'baz_';
88 |
89 | $factory = new ApcuCacheFactory(0, true, $prefix);
90 | $parser = new MoParser(__DIR__ . '/../data/little.mo');
91 |
92 | $factory->getInstance($parser, $locale, $domain);
93 |
94 | $actual = apcu_fetch($prefix . $locale . '.' . $domain . '.' . $msgid);
95 | self::assertSame($expected, $actual);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/tests/Cache/ApcuCacheTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('APCu extension is not installed and enabled for CLI');
36 | }
37 |
38 | protected function tearDown(): void
39 | {
40 | parent::tearDown();
41 |
42 | apcu_clear_cache();
43 | }
44 |
45 | public function testConstructorLoadsCache(): void
46 | {
47 | $expected = 'Pole';
48 | $locale = 'foo';
49 | $domain = 'bar';
50 | $msgid = 'Column';
51 |
52 | new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain);
53 |
54 | $actual = apcu_fetch('mo_' . $locale . '.' . $domain . '.' . $msgid);
55 | self::assertSame($expected, $actual);
56 | }
57 |
58 | public function testConstructorSetsTtl(): void
59 | {
60 | $locale = 'foo';
61 | $domain = 'bar';
62 | $msgid = 'Column';
63 | $ttl = 1;
64 |
65 | new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain, $ttl);
66 | sleep($ttl * 2);
67 |
68 | apcu_fetch('mo_' . $locale . '.' . $domain . '.' . $msgid, $success);
69 | self::assertFalse($success);
70 | apcu_fetch('mo_' . $locale . '.' . $domain . '.' . ApcuCache::LOADED_KEY, $success);
71 | self::assertFalse($success);
72 | }
73 |
74 | public function testConstructorSetsReloadOnMiss(): void
75 | {
76 | $expected = 'Column';
77 | $locale = 'foo';
78 | $domain = 'bar';
79 | $msgid = 'Column';
80 | $prefix = 'baz_';
81 |
82 | $cache = new ApcuCache(
83 | new MoParser(__DIR__ . '/../data/little.mo'),
84 | $locale,
85 | $domain,
86 | 0,
87 | false,
88 | $prefix,
89 | );
90 |
91 | apcu_delete($prefix . $locale . '.' . $domain . '.' . $msgid);
92 | $actual = $cache->get($msgid);
93 | self::assertSame($expected, $actual);
94 | }
95 |
96 | public function testConstructorSetsPrefix(): void
97 | {
98 | $expected = 'Pole';
99 | $locale = 'foo';
100 | $domain = 'bar';
101 | $msgid = 'Column';
102 | $prefix = 'baz_';
103 |
104 | new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain, 0, true, $prefix);
105 |
106 | $actual = apcu_fetch($prefix . $locale . '.' . $domain . '.' . $msgid);
107 | self::assertSame($expected, $actual);
108 | }
109 |
110 | public function testEnsureTranslationsLoadedSetsLoadedKey(): void
111 | {
112 | $expected = 1;
113 | $locale = 'foo';
114 | $domain = 'bar';
115 |
116 | new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain);
117 |
118 | $actual = apcu_fetch('mo_' . $locale . '.' . $domain . '.' . ApcuCache::LOADED_KEY);
119 | self::assertSame($expected, $actual);
120 | }
121 |
122 | public function testEnsureTranslationsLoadedHonorsLock(): void
123 | {
124 | $locale = 'foo';
125 | $domain = 'bar';
126 | $msgid = 'Column';
127 |
128 | $lock = 'mo_' . $locale . '.' . $domain . '.' . ApcuCache::LOADED_KEY;
129 | apcu_entry($lock, static function () {
130 | sleep(1);
131 |
132 | return 1;
133 | });
134 |
135 | new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain);
136 |
137 | $actual = apcu_fetch($lock);
138 | self::assertSame(1, $actual);
139 | apcu_fetch('mo_' . $locale . '.' . $domain . '.' . $msgid, $success);
140 | self::assertFalse($success);
141 | }
142 |
143 | public function testGetReturnsMsgstr(): void
144 | {
145 | $expected = 'Pole';
146 | $msgid = 'Column';
147 |
148 | $cache = new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), 'foo', 'bar');
149 |
150 | $actual = $cache->get($msgid);
151 | self::assertSame($expected, $actual);
152 | }
153 |
154 | public function testGetReturnsMsgidForCacheMiss(): void
155 | {
156 | $expected = 'Column';
157 |
158 | $cache = new ApcuCache(new MoParser(null), 'foo', 'bar');
159 |
160 | $actual = $cache->get($expected);
161 | self::assertSame($expected, $actual);
162 | }
163 |
164 | public function testStoresMsgidOnCacheMiss(): void
165 | {
166 | $expected = 'Column';
167 | $locale = 'foo';
168 | $domain = 'bar';
169 |
170 | $cache = new ApcuCache(new MoParser(null), $locale, $domain);
171 | $cache->get($expected);
172 |
173 | $actual = apcu_fetch('mo_' . $locale . '.' . $domain . '.' . $expected);
174 | self::assertSame($expected, $actual);
175 | }
176 |
177 | public function testGetReloadsOnCacheMiss(): void
178 | {
179 | $expected = 'Pole';
180 | $locale = 'foo';
181 | $domain = 'bar';
182 | $msgid = 'Column';
183 |
184 | $cache = new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), $locale, $domain);
185 |
186 | apcu_delete('mo_' . $locale . '.' . $domain . '.' . ApcuCache::LOADED_KEY);
187 | $actual = $cache->get($msgid);
188 | self::assertSame($expected, $actual);
189 | }
190 |
191 | public function testReloadOnMissHonorsLock(): void
192 | {
193 | $expected = 'Pole';
194 | $locale = 'foo';
195 | $domain = 'bar';
196 | $msgid = 'Column';
197 |
198 | $cache = new ApcuCache(new MoParser(null), $locale, $domain);
199 |
200 | $method = new ReflectionMethod($cache, 'reloadOnMiss');
201 | $method->setAccessible(true);
202 |
203 | $key = 'mo_' . $locale . '.' . $domain . '.' . $msgid;
204 | apcu_entry($key, static function () use ($expected): string {
205 | sleep(1);
206 |
207 | return $expected;
208 | });
209 | $actual = $method->invoke($cache, $msgid);
210 |
211 | self::assertSame($expected, $actual);
212 | }
213 |
214 | public function testSetSetsMsgstr(): void
215 | {
216 | $expected = 'Pole';
217 | $msgid = 'Column';
218 |
219 | $cache = new ApcuCache(new MoParser(null), 'foo', 'bar');
220 | $cache->set($msgid, $expected);
221 |
222 | $actual = $cache->get($msgid);
223 | self::assertSame($expected, $actual);
224 | }
225 |
226 | public function testHasReturnsFalse(): void
227 | {
228 | $cache = new ApcuCache(new MoParser(null), 'foo', 'bar');
229 | $actual = $cache->has('Column');
230 | self::assertFalse($actual);
231 | }
232 |
233 | public function testHasReturnsTrue(): void
234 | {
235 | $cache = new ApcuCache(new MoParser(__DIR__ . '/../data/little.mo'), 'foo', 'bar');
236 | $actual = $cache->has('Column');
237 | self::assertTrue($actual);
238 | }
239 |
240 | public function testSetAllSetsTranslations(): void
241 | {
242 | $translations = [
243 | 'foo' => 'bar',
244 | 'and' => 'another',
245 | ];
246 |
247 | $cache = new ApcuCache(new MoParser(null), 'foo', 'bar');
248 | $cache->setAll($translations);
249 |
250 | foreach ($translations as $msgid => $expected) {
251 | $actual = $cache->get($msgid);
252 | self::assertSame($expected, $actual);
253 | }
254 | }
255 |
256 | public function testCacheStoresPluralForms(): void
257 | {
258 | $expected = ['first', 'second'];
259 | $plural = ["%d pig went to the market\n", "%d pigs went to the market\n"];
260 | $msgid = implode(chr(0), $plural);
261 |
262 | $cache = new ApcuCache(new MoParser(null), 'foo', 'bar');
263 | $cache->set($msgid, implode(chr(0), $expected));
264 |
265 | $msgstr = $cache->get($msgid);
266 | $actual = explode(chr(0), $msgstr);
267 | self::assertSame($expected, $actual);
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/tests/Cache/ApcuDisabledTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('ext-apcu is enabled');
21 | }
22 |
23 | $this->expectException(CacheException::class);
24 | $this->expectExceptionMessage('APCu extension must be installed and enabled');
25 | new ApcuCache(new MoParser(null), 'foo', 'bar');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Cache/InMemoryCacheTest.php:
--------------------------------------------------------------------------------
1 | get('Column');
21 | self::assertSame($expected, $actual);
22 | }
23 |
24 | public function testGetReturnsMsgidForCacheMiss(): void
25 | {
26 | $expected = 'Column';
27 | $cache = new InMemoryCache(new MoParser(null));
28 | $actual = $cache->get($expected);
29 | self::assertSame($expected, $actual);
30 | }
31 |
32 | public function testSetSetsMsgstr(): void
33 | {
34 | $expected = 'Pole';
35 | $msgid = 'Column';
36 | $cache = new InMemoryCache(new MoParser(null));
37 | $cache->set($msgid, $expected);
38 | $actual = $cache->get($msgid);
39 | self::assertSame($expected, $actual);
40 | }
41 |
42 | public function testHasReturnsFalse(): void
43 | {
44 | $cache = new InMemoryCache(new MoParser(null));
45 | $actual = $cache->has('Column');
46 | self::assertFalse($actual);
47 | }
48 |
49 | public function testHasReturnsTrue(): void
50 | {
51 | $cache = new InMemoryCache(new MoParser(__DIR__ . '/../data/little.mo'));
52 | $actual = $cache->has('Column');
53 | self::assertTrue($actual);
54 | }
55 |
56 | public function testSetAllSetsTranslations(): void
57 | {
58 | $translations = [
59 | 'foo' => 'bar',
60 | 'and' => 'another',
61 | ];
62 | $cache = new InMemoryCache(new MoParser(null));
63 | $cache->setAll($translations);
64 | foreach ($translations as $msgid => $expected) {
65 | $actual = $cache->get($msgid);
66 | self::assertSame($expected, $actual);
67 | }
68 | }
69 |
70 | public function testGetAllReturnsTranslations(): void
71 | {
72 | $expected = [
73 | 'foo' => 'bar',
74 | 'and' => 'another',
75 | ];
76 | $cache = new InMemoryCache(new MoParser(null));
77 | $cache->setAll($expected);
78 | $actual = $cache->getAll();
79 | self::assertSame($expected, $actual);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/FunctionsTest.php:
--------------------------------------------------------------------------------
1 | $expected */
24 | #[DataProvider('localeList')]
25 | public function testListLocales(string $locale, array $expected): void
26 | {
27 | self::assertSame($expected, Loader::listLocales($locale));
28 | }
29 |
30 | /** @return list}> */
31 | public static function localeList(): array
32 | {
33 | return [
34 | [
35 | 'cs_CZ',
36 | [
37 | 'cs_CZ',
38 | 'cs',
39 | ],
40 | ],
41 | [
42 | 'sr_CS.UTF-8@latin',
43 | [
44 | 'sr_CS.UTF-8@latin',
45 | 'sr_CS@latin',
46 | 'sr@latin',
47 | 'sr_CS.UTF-8',
48 | 'sr_CS',
49 | 'sr',
50 | ],
51 | ],
52 | // For a locale containing country code, we prefer
53 | // full locale name, but if that's not found, fall back
54 | // to the language only locale name.
55 | [
56 | 'sr_RS',
57 | [
58 | 'sr_RS',
59 | 'sr',
60 | ],
61 | ],
62 | // If language code is used, it's the only thing returned.
63 | [
64 | 'sr',
65 | ['sr'],
66 | ],
67 | // There is support for language and charset only.
68 | [
69 | 'sr.UTF-8',
70 | [
71 | 'sr.UTF-8',
72 | 'sr',
73 | ],
74 | ],
75 |
76 | // It can also split out character set from the full locale name.
77 | [
78 | 'sr_RS.UTF-8',
79 | [
80 | 'sr_RS.UTF-8',
81 | 'sr_RS',
82 | 'sr',
83 | ],
84 | ],
85 |
86 | // There is support for @modifier in locale names as well.
87 | [
88 | 'sr_RS.UTF-8@latin',
89 | [
90 | 'sr_RS.UTF-8@latin',
91 | 'sr_RS@latin',
92 | 'sr@latin',
93 | 'sr_RS.UTF-8',
94 | 'sr_RS',
95 | 'sr',
96 | ],
97 | ],
98 | [
99 | 'sr.UTF-8@latin',
100 | [
101 | 'sr.UTF-8@latin',
102 | 'sr@latin',
103 | 'sr.UTF-8',
104 | 'sr',
105 | ],
106 | ],
107 |
108 | // We can pass in only language and modifier.
109 | [
110 | 'sr@latin',
111 | [
112 | 'sr@latin',
113 | 'sr',
114 | ],
115 | ],
116 |
117 | // If locale name is not following the regular POSIX pattern,
118 | // it's used verbatim.
119 | [
120 | 'something',
121 | ['something'],
122 | ],
123 |
124 | // Passing in an empty string returns an empty array.
125 | [
126 | '',
127 | [],
128 | ],
129 | ];
130 | }
131 |
132 | private function getLoader(string $domain, string $locale): Loader
133 | {
134 | $loader = new Loader();
135 | $loader->setlocale($locale);
136 | $loader->textdomain($domain);
137 | $loader->bindtextdomain($domain, __DIR__ . '/data/locale/');
138 |
139 | return $loader;
140 | }
141 |
142 | public function testLocaleChange(): void
143 | {
144 | $loader = new Loader();
145 | $loader->setlocale('cs');
146 | $loader->textdomain('phpmyadmin');
147 | $loader->bindtextdomain('phpmyadmin', __DIR__ . '/data/locale/');
148 | $translator = $loader->getTranslator('phpmyadmin');
149 | self::assertSame('Typ', $translator->gettext('Type'));
150 | $loader->setlocale('be_BY');
151 | $translator = $loader->getTranslator('phpmyadmin');
152 | self::assertSame('Тып', $translator->gettext('Type'));
153 | }
154 |
155 | #[DataProvider('translatorData')]
156 | public function testGetTranslator(string $domain, string $locale, string $otherdomain, string $expected): void
157 | {
158 | $loader = $this->getLoader($domain, $locale);
159 | $translator = $loader->getTranslator($otherdomain);
160 | self::assertSame(
161 | $expected,
162 | $translator->gettext('Type'),
163 | );
164 | }
165 |
166 | /** @return list */
167 | public static function translatorData(): array
168 | {
169 | return [
170 | [
171 | 'phpmyadmin',
172 | 'cs',
173 | '',
174 | 'Typ',
175 | ],
176 | [
177 | 'phpmyadmin',
178 | 'cs_CZ',
179 | '',
180 | 'Typ',
181 | ],
182 | [
183 | 'phpmyadmin',
184 | 'be_BY',
185 | '',
186 | 'Тып',
187 | ],
188 | [
189 | 'phpmyadmin',
190 | 'be@latin',
191 | '',
192 | 'Typ',
193 | ],
194 | [
195 | 'phpmyadmin',
196 | 'cs',
197 | 'other',
198 | 'Type',
199 | ],
200 | [
201 | 'other',
202 | 'cs',
203 | 'phpmyadmin',
204 | 'Type',
205 | ],
206 | ];
207 | }
208 |
209 | public function testInstance(): void
210 | {
211 | $loader = Loader::getInstance();
212 | $loader->setlocale('cs');
213 | $loader->textdomain('phpmyadmin');
214 | $loader->bindtextdomain('phpmyadmin', __DIR__ . '/data/locale/');
215 |
216 | $translator = $loader->getTranslator();
217 | self::assertSame(
218 | 'Typ',
219 | $translator->gettext('Type'),
220 | );
221 |
222 | /* Ensure the object survives */
223 | $loader = Loader::getInstance();
224 | $translator = $loader->getTranslator();
225 | self::assertSame(
226 | 'Typ',
227 | $translator->gettext('Type'),
228 | );
229 |
230 | /* Ensure the object can support different locale files for the same domain */
231 | $loader = Loader::getInstance();
232 | $loader->setlocale('be_BY');
233 | $loader->bindtextdomain('phpmyadmin', __DIR__ . '/data/locale/');
234 | $translator = $loader->getTranslator();
235 | self::assertSame(
236 | 'Тып',
237 | $translator->gettext('Type'),
238 | );
239 | }
240 |
241 | public function testDetect(): void
242 | {
243 | $GLOBALS['lang'] = 'foo';
244 | $loader = Loader::getInstance();
245 | self::assertSame(
246 | 'foo',
247 | $loader->detectlocale(),
248 | );
249 | unset($GLOBALS['lang']);
250 | }
251 |
252 | public function testDetectEnv(): void
253 | {
254 | $loader = Loader::getInstance();
255 | foreach (['LC_MESSAGES', 'LC_ALL', 'LANG'] as $var) {
256 | putenv($var);
257 | if (getenv($var) === false) {
258 | continue;
259 | }
260 |
261 | $this->markTestSkipped('Unsetting environment does not work');
262 | }
263 |
264 | unset($GLOBALS['lang']);
265 | putenv('LC_ALL=baz');
266 | self::assertSame(
267 | 'baz',
268 | $loader->detectlocale(),
269 | );
270 | putenv('LC_ALL');
271 | putenv('LC_MESSAGES=bar');
272 | self::assertSame(
273 | 'bar',
274 | $loader->detectlocale(),
275 | );
276 | putenv('LC_MESSAGES');
277 | putenv('LANG=barr');
278 | self::assertSame(
279 | 'barr',
280 | $loader->detectlocale(),
281 | );
282 | putenv('LANG');
283 | self::assertSame(
284 | 'en',
285 | $loader->detectlocale(),
286 | );
287 | }
288 |
289 | public function testSetCacheFactory(): void
290 | {
291 | $expected = 'Foo';
292 | $locale = 'be_BY';
293 | $domain = 'apcu';
294 |
295 | $cache = $this->createMock(CacheInterface::class);
296 | $cache->method('get')
297 | ->willReturn($expected);
298 | /** @var CacheFactoryInterface&MockObject $factory */
299 | $factory = $this->createMock(CacheFactoryInterface::class);
300 | $factory->expects($this->once())
301 | ->method('getInstance')
302 | ->with($this->isInstanceOf(MoParser::class), $locale, $domain)
303 | ->willReturn($cache);
304 |
305 | Loader::setCacheFactory($factory);
306 | $loader = Loader::getInstance();
307 | $loader->setlocale($locale);
308 | $loader->bindtextdomain($domain, __DIR__ . '/data/locale/');
309 | $translator = $loader->getTranslator($domain);
310 |
311 | $actual = $translator->gettext('Type');
312 | self::assertSame($expected, $actual);
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/tests/MoFilesTest.php:
--------------------------------------------------------------------------------
1 | getTranslator($filename);
26 | self::assertSame(
27 | 'Pole',
28 | $parser->gettext('Column'),
29 | );
30 | // Non existing string
31 | self::assertSame(
32 | 'Column parser',
33 | $parser->gettext('Column parser'),
34 | );
35 | }
36 |
37 | #[DataProvider('provideMoFiles')]
38 | public function testMoFilePlurals(string $filename): void
39 | {
40 | $parser = $this->getTranslator($filename);
41 | $expected2 = '%d sekundy';
42 | if (str_contains($filename, 'invalid-formula.mo') || str_contains($filename, 'lessplurals.mo')) {
43 | $expected0 = '%d sekunda';
44 | $expected2 = '%d sekunda';
45 | } elseif (str_contains($filename, 'plurals.mo') || str_contains($filename, 'noheader.mo')) {
46 | $expected0 = '%d sekundy';
47 | } else {
48 | $expected0 = '%d sekund';
49 | }
50 |
51 | self::assertSame($expected0, $parser->ngettext('%d second', '%d seconds', 0));
52 | self::assertSame('%d sekunda', $parser->ngettext('%d second', '%d seconds', 1));
53 | self::assertSame($expected2, $parser->ngettext('%d second', '%d seconds', 2));
54 | self::assertSame($expected0, $parser->ngettext('%d second', '%d seconds', 5));
55 | self::assertSame($expected0, $parser->ngettext('%d second', '%d seconds', 10));
56 | // Non existing string
57 | self::assertSame('"%d" seconds', $parser->ngettext('"%d" second', '"%d" seconds', 10));
58 | }
59 |
60 | #[DataProvider('provideMoFiles')]
61 | public function testMoFileContext(string $filename): void
62 | {
63 | $parser = $this->getTranslator($filename);
64 | self::assertSame('Tabulka', $parser->pgettext('Display format', 'Table'));
65 | }
66 |
67 | #[DataProvider('provideNotTranslatedFiles')]
68 | public function testMoFileNotTranslated(string $filename): void
69 | {
70 | $parser = $this->getTranslator($filename);
71 | self::assertSame('%d second', $parser->ngettext('%d second', '%d seconds', 1));
72 | }
73 |
74 | /** @return list */
75 | public static function provideMoFiles(): array
76 | {
77 | return self::getFiles('./tests/data/*.mo');
78 | }
79 |
80 | /** @return list */
81 | public static function provideErrorMoFiles(): array
82 | {
83 | return self::getFiles('./tests/data/error/*.mo');
84 | }
85 |
86 | /** @return list */
87 | public static function provideNotTranslatedFiles(): array
88 | {
89 | return self::getFiles('./tests/data/not-translated/*.mo');
90 | }
91 |
92 | #[DataProvider('provideErrorMoFiles')]
93 | public function testEmptyMoFile(string $file): void
94 | {
95 | $parser = new MoParser($file);
96 | $translator = new Translator(new InMemoryCache($parser));
97 | if (basename($file) === 'magic.mo') {
98 | self::assertSame(Translator::ERROR_BAD_MAGIC, $parser->error);
99 | } else {
100 | self::assertSame(Translator::ERROR_READING, $parser->error);
101 | }
102 |
103 | self::assertSame('Table', $translator->pgettext('Display format', 'Table'));
104 | self::assertSame('"%d" seconds', $translator->ngettext('"%d" second', '"%d" seconds', 10));
105 | }
106 |
107 | #[DataProvider('provideMoFiles')]
108 | public function testExists(string $file): void
109 | {
110 | $parser = $this->getTranslator($file);
111 | self::assertTrue($parser->exists('Column'));
112 | self::assertFalse($parser->exists('Column parser'));
113 | }
114 |
115 | /**
116 | * @param string $pattern path names pattern to match
117 | *
118 | * @return list
119 | */
120 | private static function getFiles(string $pattern): array
121 | {
122 | $files = glob($pattern);
123 | if ($files === false) {
124 | return [];
125 | }
126 |
127 | $result = [];
128 | foreach ($files as $file) {
129 | $result[] = [$file];
130 | }
131 |
132 | return $result;
133 | }
134 |
135 | private function getTranslator(string $filename): Translator
136 | {
137 | return new Translator(new InMemoryCache(new MoParser($filename)));
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/tests/PluralFormulaTest.php:
--------------------------------------------------------------------------------
1 | */
29 | public static function pluralExtractionData(): array
30 | {
31 | return [
32 | // It defaults to a "Western-style" plural header.
33 | [
34 | '',
35 | 'nplurals=2; plural=n == 1 ? 0 : 1;',
36 | ],
37 | // Extracting it from the middle of the header works.
38 | [
39 | "Content-type: text/html; charset=UTF-8\n"
40 | . "Plural-Forms: nplurals=1; plural=0;\n"
41 | . "Last-Translator: nobody\n",
42 | ' nplurals=1; plural=0;',
43 | ],
44 | // It's also case-insensitive.
45 | [
46 | "PLURAL-forms: nplurals=1; plural=0;\n",
47 | ' nplurals=1; plural=0;',
48 | ],
49 | // It falls back to default if it's not on a separate line.
50 | [
51 | 'Content-type: text/html; charset=UTF-8' // note the missing \n here
52 | . "Plural-Forms: nplurals=1; plural=0;\n"
53 | . "Last-Translator: nobody\n",
54 | 'nplurals=2; plural=n == 1 ? 0 : 1;',
55 | ],
56 | ];
57 | }
58 |
59 | #[DataProvider('pluralCounts')]
60 | public function testPluralCounts(string $expr, int $expected): void
61 | {
62 | self::assertSame(
63 | $expected,
64 | Translator::extractPluralCount($expr),
65 | );
66 | }
67 |
68 | /** @return list */
69 | public static function pluralCounts(): array
70 | {
71 | return [
72 | [
73 | '',
74 | 1,
75 | ],
76 | [
77 | 'foo=2; expr',
78 | 1,
79 | ],
80 | [
81 | 'nplurals=2; epxr',
82 | 2,
83 | ],
84 | [
85 | ' nplurals = 3 ; epxr',
86 | 3,
87 | ],
88 | [
89 | ' nplurals = 4 ; epxr ; ',
90 | 4,
91 | ],
92 | [
93 | 'nplurals',
94 | 1,
95 | ],
96 | ];
97 | }
98 |
99 | #[DataProvider('pluralExpressions')]
100 | public function testPluralExpression(string $expr, string $expected): void
101 | {
102 | self::assertSame(
103 | $expected,
104 | Translator::sanitizePluralExpression($expr),
105 | );
106 | }
107 |
108 | /** @return list */
109 | public static function pluralExpressions(): array
110 | {
111 | return [
112 | [
113 | '',
114 | '',
115 | ],
116 | [
117 | 'nplurals=2; plural=n == 1 ? 0 : 1;',
118 | 'n == 1 ? 0 : 1',
119 | ],
120 | [
121 | ' nplurals=1; plural=0;',
122 | '0',
123 | ],
124 | [
125 | "nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n",
126 | 'n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5',
127 | ],
128 | [
129 | ' nplurals=1; plural=baz(n);',
130 | '(n)',
131 | ],
132 | [
133 | ' plural=n',
134 | 'n',
135 | ],
136 | [
137 | 'nplurals',
138 | 'n',
139 | ],
140 | ];
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/tests/PluralTest.php:
--------------------------------------------------------------------------------
1 | getTranslator('');
31 | $result = $parser->npgettext('context', "%d pig went to the market\n", "%d pigs went to the market\n", $number);
32 | self::assertSame($expected, $result);
33 | }
34 |
35 | /**
36 | * Data provider for test_npgettext.
37 | *
38 | * @return list
39 | */
40 | public static function providerTestNpgettext(): array
41 | {
42 | return [
43 | [
44 | 1,
45 | "%d pig went to the market\n",
46 | ],
47 | [
48 | 2,
49 | "%d pigs went to the market\n",
50 | ],
51 | ];
52 | }
53 |
54 | /**
55 | * Test for ngettext
56 | */
57 | public function testNgettext(): void
58 | {
59 | $parser = $this->getTranslator('');
60 | $translationKey = implode(chr(0), ["%d pig went to the market\n", "%d pigs went to the market\n"]);
61 | $parser->setTranslation($translationKey, '');
62 | $result = $parser->ngettext("%d pig went to the market\n", "%d pigs went to the market\n", 1);
63 | self::assertSame('', $result);
64 | }
65 |
66 | /** @return list */
67 | public static function dataProviderPluralForms(): array
68 | {
69 | return [
70 | ['Plural-Forms: nplurals=2; plural=n != 1;'],
71 | ['Plural-Forms: nplurals=1; plural=0;'],
72 | ['Plural-Forms: nplurals=2; plural=(n > 1);'],
73 | [
74 | 'Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n'
75 | . '%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;',
76 | ],
77 | ['Plural-Forms: nplurals=2; plural=n >= 2 && (n < 11 || n > 99);'],
78 | ['Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;'],
79 | ['Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;'],
80 | [
81 | 'Plural-Forms: nplurals=2; plural=n != 1 && n != 2 && n != 3 &'
82 | . '& (n % 10 == 4 || n % 10 == 6 || n % 10 == 9);',
83 | ],
84 | ];
85 | }
86 |
87 | /**
88 | * Test for ngettext
89 | *
90 | * @see https://github.com/phpmyadmin/motranslator/issues/37
91 | */
92 | #[DataProvider('dataProviderPluralForms')]
93 | public function testNgettextSelectString(string $pluralForms): void
94 | {
95 | $parser = $this->getTranslator('');
96 | $parser->setTranslation(
97 | '',
98 | "Project-Id-Version: phpMyAdmin 5.1.0-dev\n"
99 | . "Report-Msgid-Bugs-To: translators@phpmyadmin.net\n"
100 | . "PO-Revision-Date: 2020-09-01 09:12+0000\n"
101 | . "Last-Translator: William Desportes \n"
102 | . 'Language-Team: English (United Kingdom) '
103 | . "\n"
104 | . "Language: en_GB\n"
105 | . "MIME-Version: 1.0\n"
106 | . "Content-Type: text\/plain; charset=UTF-8\n"
107 | . "Content-Transfer-Encoding: 8bit\n"
108 | . $pluralForms . "\n"
109 | . "X-Generator: Weblate 4.2.1-dev\n"
110 | . '',
111 | );
112 | $translationKey = implode(chr(0), ["%d pig went to the market\n", "%d pigs went to the market\n"]);
113 | $parser->setTranslation($translationKey, 'ok');
114 | $result = $parser->ngettext("%d pig went to the market\n", "%d pigs went to the market\n", 1);
115 | self::assertSame('ok', $result);
116 | }
117 |
118 | private function getTranslator(string $filename): Translator
119 | {
120 | return new Translator(new InMemoryCache(new MoParser($filename)));
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/StringReaderTest.php:
--------------------------------------------------------------------------------
1 | read(-1, -1);
24 | self::assertSame('', $actual);
25 | }
26 |
27 | public function testReadIntArray(): void
28 | {
29 | $tempFile = (string) tempnam(sys_get_temp_dir(), 'phpMyAdmin_StringReaderTest');
30 | file_put_contents($tempFile, "\0\0\0\0\0\0\0\0\0\0\0\0");
31 | self::assertFileExists($tempFile);
32 | $stringReader = new StringReader($tempFile);
33 | unlink($tempFile);
34 | $actual = $stringReader->readintarray('V', 2, 2);
35 | self::assertSame([
36 | 1 => 0,
37 | 2 => 0,
38 | ], $actual);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/TranslatorTest.php:
--------------------------------------------------------------------------------
1 | gettext('Column');
25 | self::assertSame($expected, $actual);
26 | }
27 |
28 | public function testConstructorWithNullParam(): void
29 | {
30 | $expected = 'Column';
31 | $translator = new Translator(null);
32 | $actual = $translator->gettext($expected);
33 | self::assertSame($expected, $actual);
34 | }
35 |
36 | /**
37 | * Test on empty gettext
38 | */
39 | public function testGettext(): void
40 | {
41 | $translator = $this->getTranslator('');
42 | self::assertSame('Test', $translator->gettext('Test'));
43 | }
44 |
45 | /**
46 | * Test set a translation
47 | */
48 | public function testSetTranslation(): void
49 | {
50 | $translator = $this->getTranslator('');
51 | $translator->setTranslation('Test', 'Translation');
52 | self::assertSame('Translation', $translator->gettext('Test'));
53 | }
54 |
55 | /**
56 | * Test get and set all translations
57 | */
58 | public function testGetSetTranslations(): void
59 | {
60 | $transTable = ['Test' => 'Translation'];
61 | $translator = $this->getTranslator('');
62 | $translator->setTranslations($transTable);
63 | self::assertSame('Translation', $translator->gettext('Test'));
64 | self::assertSame($transTable, $translator->getTranslations());
65 | $translator = $this->getTranslator(null);
66 | $translator->setTranslations($transTable);
67 | self::assertSame($transTable, $translator->getTranslations());
68 | self::assertSame('Translation', $translator->gettext('Test'));
69 | $transTable = [
70 | 'Test' => 'Translation',
71 | 'shouldIWriteTests' => 'as much as possible',
72 | 'is it hard' => 'it depends',
73 | ];
74 | $translator = $this->getTranslator('');
75 | $translator->setTranslations($transTable);
76 | self::assertSame($transTable, $translator->getTranslations());
77 | self::assertSame('as much as possible', $translator->gettext('shouldIWriteTests'));
78 | $translator = $this->getTranslator(null);
79 | $translator->setTranslations($transTable);
80 | self::assertSame($transTable, $translator->getTranslations());
81 | self::assertSame('it depends', $translator->gettext('is it hard'));
82 | }
83 |
84 | public function testGetTranslationsThrowsException(): void
85 | {
86 | /** @var CacheInterface&MockObject $cache */
87 | $cache = $this->createMock(CacheInterface::class);
88 | $translator = new Translator($cache);
89 |
90 | $this->expectException(CacheException::class);
91 | $translator->getTranslations();
92 | }
93 |
94 | private function getTranslator(string|null $filename): Translator
95 | {
96 | return new Translator(new InMemoryCache(new MoParser($filename)));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/data/big.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/big.mo
--------------------------------------------------------------------------------
/tests/data/error/big.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/error/big.mo
--------------------------------------------------------------------------------
/tests/data/error/dos.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/error/dos.mo
--------------------------------------------------------------------------------
/tests/data/error/empty.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/error/empty.mo
--------------------------------------------------------------------------------
/tests/data/error/fpd.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/error/fpd.mo
--------------------------------------------------------------------------------
/tests/data/error/fpdle.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/error/fpdle.mo
--------------------------------------------------------------------------------
/tests/data/error/magic.mo:
--------------------------------------------------------------------------------
1 | 1234
2 |
--------------------------------------------------------------------------------
/tests/data/invalid-formula.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/invalid-formula.mo
--------------------------------------------------------------------------------
/tests/data/lessplurals.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/lessplurals.mo
--------------------------------------------------------------------------------
/tests/data/little.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/little.mo
--------------------------------------------------------------------------------
/tests/data/locale/be/LC_MESSAGES/phpmyadmin.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/locale/be/LC_MESSAGES/phpmyadmin.mo
--------------------------------------------------------------------------------
/tests/data/locale/be@latin/LC_MESSAGES/phpmyadmin.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/locale/be@latin/LC_MESSAGES/phpmyadmin.mo
--------------------------------------------------------------------------------
/tests/data/locale/cs/LC_MESSAGES/phpmyadmin.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/locale/cs/LC_MESSAGES/phpmyadmin.mo
--------------------------------------------------------------------------------
/tests/data/noheader.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/noheader.mo
--------------------------------------------------------------------------------
/tests/data/not-translated/fpd1.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/not-translated/fpd1.mo
--------------------------------------------------------------------------------
/tests/data/not-translated/invalid-equation.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/not-translated/invalid-equation.mo
--------------------------------------------------------------------------------
/tests/data/plurals.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpmyadmin/motranslator/28b03785c0d298897bba74f4a35467dbb126ffdb/tests/data/plurals.mo
--------------------------------------------------------------------------------