├── .github └── workflows │ └── test.yml ├── .gitignore ├── 35_change_load_order_locales.rb ├── Gemfile ├── LICENSE ├── README.md ├── app ├── controllers │ └── custom_message_settings_controller.rb ├── helpers │ └── custom_message_settings_helper.rb ├── models │ └── custom_message_setting.rb └── views │ └── custom_message_settings │ ├── _messages.html.erb │ ├── _normal_tab.html.erb │ ├── _yaml_tab.html.erb │ ├── default_messages.html.erb │ ├── edit.html.erb │ └── edit.js.erb ├── assets ├── javascripts │ └── select2.min.js └── stylesheets │ ├── custom_messages.css │ ├── default_messages.css │ └── select2.min.css ├── changelog.md ├── config ├── locales │ ├── custom_messages │ │ ├── ar.rb │ │ ├── az.rb │ │ ├── bg.rb │ │ ├── bs.rb │ │ ├── ca.rb │ │ ├── cs.rb │ │ ├── da.rb │ │ ├── de.rb │ │ ├── el.rb │ │ ├── en-GB.rb │ │ ├── en.rb │ │ ├── es-PA.rb │ │ ├── es.rb │ │ ├── et.rb │ │ ├── eu.rb │ │ ├── fa.rb │ │ ├── fi.rb │ │ ├── fr.rb │ │ ├── gl.rb │ │ ├── he.rb │ │ ├── hr.rb │ │ ├── hu.rb │ │ ├── id.rb │ │ ├── it.rb │ │ ├── ja.rb │ │ ├── ko.rb │ │ ├── lt.rb │ │ ├── lv.rb │ │ ├── mk.rb │ │ ├── mn.rb │ │ ├── nl.rb │ │ ├── no.rb │ │ ├── pl.rb │ │ ├── pt-BR.rb │ │ ├── pt.rb │ │ ├── ro.rb │ │ ├── ru.rb │ │ ├── sk.rb │ │ ├── sl.rb │ │ ├── sq.rb │ │ ├── sr-YU.rb │ │ ├── sr.rb │ │ ├── sv.rb │ │ ├── ta-IN.rb │ │ ├── th.rb │ │ ├── tr.rb │ │ ├── uk.rb │ │ ├── vi.rb │ │ ├── zh-TW.rb │ │ └── zh.rb │ ├── de.yml │ ├── en.yml │ └── ja.yml └── routes.rb ├── init.rb ├── lib └── message_customize │ ├── application_controller_patch.rb │ ├── hooks.rb │ └── locale.rb └── test ├── fixtures └── custom_message_settings.yml ├── functional └── custom_message_settings_controller_test.rb ├── integration └── application_controller_patch_test.rb ├── test_helper.rb └── unit ├── custom_message_setting_test.rb └── lib └── message_customize └── locale_test.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | include: 14 | - redmine-repository: 'redmica/redmica' 15 | redmine-version: 'master' 16 | ruby-version: '3.3' 17 | - redmine-repository: 'redmine/redmine' 18 | redmine-version: 'master' 19 | ruby-version: '3.3' 20 | 21 | steps: 22 | - uses: hidakatsuya/action-setup-redmine@v1 23 | with: 24 | repository: ${{ matrix.redmine-repository }} 25 | version: ${{ matrix.redmine-version }} 26 | ruby-version: ${{ matrix.ruby-version }} 27 | database: 'postgres:14' 28 | 29 | - uses: actions/checkout@v4 30 | with: 31 | path: plugins/redmine_message_customize 32 | 33 | - name: Set up plugin 34 | run: bundle install 35 | 36 | - name: Run tests 37 | run: bin/rails redmine:plugins:test NAME=redmine_message_customize 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | tmp/ 3 | -------------------------------------------------------------------------------- /35_change_load_order_locales.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redmine/plugin' 4 | if Redmine::Plugin.installed? :redmine_message_customize 5 | p = Redmine::Plugin.find(:redmine_message_customize) 6 | custom_locales = Dir.glob(File.join(p.directory, 'config', 'locales', 'custom_messages', '*.rb')) 7 | Rails.application.config.i18n.load_path = (Rails.application.config.i18n.load_path - custom_locales + custom_locales) 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | if dependencies.detect { |d| d.name == 'rails' && d.requirement.to_s.include?('4.2.') } 2 | gem 'rails_kwargs_testing', group: :test 3 | end 4 | -------------------------------------------------------------------------------- /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 | 294 | Copyright (C) 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 | , 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redmine_message_customize 2 | 3 | This is a plugin for Redmine. 4 | This plugin changes the translation of the wording on the screen defined in "config/locales/*.yml" in the admin view. 5 | It is available for Redmine 6.0 or later. 6 | 7 | ## Install 8 | 9 | ``` 10 | $ cd /your/path/redmine 11 | $ git clone https://github.com/farend/redmine_message_customize.git plugins/redmine_message_customize 12 | $ # redmine restart 13 | ``` 14 | 15 | ## Usage 16 | 17 | * 1: Open setting page 18 | Administration > Message customize 19 | 20 | 21 | * 2-1: Normal mode tab 22 | 23 | 24 | * 2-2: YAML mode tab 25 | 26 | 27 | ## Run test 28 | 29 | ``` 30 | $ cd /your/path/redmine 31 | $ bundle exec rake redmine:plugins:test NAME=redmine_message_customize RAILS_ENV=test 32 | ``` 33 | 34 | ---- 35 | 36 | 2022/6/08 Transferred ownership of the repository from ishikawa999 to [Far End Technologies Corporation](https://github.com/farend). 37 | -------------------------------------------------------------------------------- /app/controllers/custom_message_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class CustomMessageSettingsController < ApplicationController 2 | layout 'admin' 3 | menu_item :custom_messages 4 | self.main_menu = false 5 | before_action :require_admin, :set_custom_message_setting, :set_lang 6 | require_sudo_mode :edit, :update, :toggle_enabled, :default_messages 7 | 8 | def edit 9 | end 10 | 11 | def default_messages 12 | @file_path = Rails.root.join('config', 'locales', "#{@lang}.yml") 13 | end 14 | 15 | def update 16 | if setting_params.key?(:custom_messages) || params[:tab] == 'normal' 17 | @setting.update_with_custom_messages(setting_params[:custom_messages].try(:to_unsafe_h).try(:to_hash) || {}, @lang) 18 | elsif setting_params.key?(:custom_messages_yaml) 19 | @setting.update_with_custom_messages_yaml(setting_params[:custom_messages_yaml]) 20 | end 21 | 22 | if @setting.errors.blank? 23 | flash[:notice] = l(:notice_successful_update) 24 | redirect_to edit_custom_message_settings_path(tab: params[:tab], lang: @lang) 25 | else 26 | render :edit 27 | end 28 | 29 | # Catch an exception that occurs when the value field capacity is exceeded (ActiveRecord::ValueTooLong) 30 | rescue ActiveRecord::StatementInvalid 31 | render_error l(:error_value_too_long) 32 | end 33 | 34 | def toggle_enabled 35 | if @setting.toggle_enabled! 36 | flash[:notice] = 37 | @setting.enabled? ? l(:notice_enabled_customize) : l(:notice_disabled_customize) 38 | redirect_to edit_custom_message_settings_path 39 | else 40 | render :edit 41 | end 42 | end 43 | 44 | private 45 | 46 | def set_custom_message_setting 47 | @setting = CustomMessageSetting.find_or_default 48 | end 49 | 50 | def setting_params 51 | params.fetch(:settings, {}) 52 | end 53 | 54 | def set_lang 55 | @lang = 56 | MessageCustomize::Locale.find_language( 57 | params[:lang].presence || @setting.custom_messages.keys.first || current_user_language 58 | ) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/helpers/custom_message_settings_helper.rb: -------------------------------------------------------------------------------- 1 | module CustomMessageSettingsHelper 2 | def available_message_options(setting, lang) 3 | options = [['', '']] + 4 | CustomMessageSetting.flatten_hash(MessageCustomize::Locale.available_messages(lang)) 5 | .select{|_k, v| v.is_a?(String)} 6 | .map{|k, v| ["#{k}: #{v}", k]} 7 | 8 | options_for_select(options, disabled: setting.custom_messages_to_flatten_hash(lang).keys) 9 | end 10 | 11 | def normal_mode_input_fields(setting, lang) 12 | return '' if setting.custom_messages.is_a?(String) || setting.custom_messages.blank? 13 | 14 | content = ActiveSupport::SafeBuffer.new 15 | custom_messages_hash = setting.custom_messages_to_flatten_hash(lang.to_s) 16 | custom_messages_hash.each do |k, v| 17 | content += content_tag(:p) do 18 | content_tag(:label, k) + 19 | text_field_tag("settings[custom_messages][#{k}]", v.to_s) + 20 | link_to_function(sprite_icon('del'), '$(this).closest("p").remove()', class: 'icon clear-key-link') 21 | end 22 | end 23 | content 24 | end 25 | 26 | def open_default_messages_window_link(lang) 27 | link_to sprite_icon('file', l(:label_default_messages)), 28 | default_messages_custom_message_settings_path(lang: lang), 29 | class: 'icon text-plain', 30 | onclick: "window.open(this.href,'redmine_message_customize_plugin-default_messages', 'height=800, width=500');return false;", 31 | id: 'default-messages-link' 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/custom_message_setting.rb: -------------------------------------------------------------------------------- 1 | class CustomMessageSetting < Setting 2 | validate :convertible_to_yaml, 3 | :custom_message_languages_are_available, :custom_message_keys_are_available 4 | 5 | def self.find_or_default 6 | super('plugin_redmine_message_customize') 7 | end 8 | 9 | def enabled? 10 | self.value[:enabled] != 'false' 11 | end 12 | 13 | def custom_messages(lang=nil, check_enabled=false) 14 | return {} if check_enabled && !self.enabled? 15 | 16 | messages = raw_custom_messages 17 | messages = messages[lang.to_s] if lang.present? 18 | messages || {} 19 | end 20 | 21 | def custom_messages_with_timestamp(lang) 22 | messages = self.custom_messages(lang, true) 23 | messages.merge({'redmine_message_customize_timestamp' => self.try(:updated_on).to_i.to_s}) 24 | end 25 | 26 | def latest_messages_applied?(lang) 27 | return true if self.new_record? 28 | 29 | redmine_message_customize_timestamp = I18n.backend.send(:translations)[:"#{lang}"]&.[](:redmine_message_customize_timestamp) 30 | redmine_message_customize_timestamp == self.updated_on.to_i.to_s 31 | end 32 | 33 | def custom_messages_to_flatten_hash(lang=nil) 34 | self.class.flatten_hash(custom_messages(lang)) 35 | end 36 | 37 | def custom_messages_to_yaml 38 | messages = custom_messages 39 | if messages.is_a?(Hash) 40 | messages.present? ? YAML.dump(messages) : '' 41 | else 42 | raw_custom_messages 43 | end 44 | end 45 | 46 | def update_with_custom_messages(custom_messages, lang) 47 | value = CustomMessageSetting.nested_hash(custom_messages) 48 | original_custom_messages = self.custom_messages 49 | messages = 50 | if value.present? 51 | original_custom_messages.merge({lang => value}) 52 | else 53 | original_custom_messages.delete(lang) 54 | original_custom_messages 55 | end 56 | 57 | self.custom_messages = messages 58 | self.save 59 | end 60 | 61 | def update_with_custom_messages_yaml(yaml) 62 | self.custom_messages = yaml 63 | self.save 64 | end 65 | 66 | def toggle_enabled! 67 | self.value = self.value.merge({enabled: (!self.enabled?).to_s}) 68 | self.save 69 | end 70 | 71 | # { date: { formats: { defaults: '%m/%d/%Y'}}} to {'date.formats.defaults' => '%m/%d/%Y'} 72 | def self.flatten_hash(hash=nil) 73 | return hash unless hash.is_a?(Hash) 74 | 75 | hash.each_with_object({}) do |(key, value), content| 76 | next self.flatten_hash(value).each do |k, v| 77 | content[:"#{key}.#{k}"] = v 78 | end if value.is_a? Hash 79 | content[key] = value 80 | end 81 | end 82 | 83 | # {'date.formats.defaults' => '%m/%d/%Y'} to { date: { formats: { defaults: '%m/%d/%Y'}}} 84 | def self.nested_hash(hash=nil) 85 | new_hash = {} 86 | hash.each do |key, value| 87 | h = value 88 | key.to_s.split('.').reverse_each do |k| 89 | h = {k => h} 90 | end 91 | new_hash = new_hash.deep_merge(h) 92 | end 93 | new_hash 94 | end 95 | 96 | private 97 | 98 | def raw_custom_messages 99 | self.value[:custom_messages] || self.value['custom_messages'] 100 | end 101 | 102 | def custom_messages=(messages) 103 | messages = YAML.load("#{messages}") unless messages.is_a?(Hash) 104 | self.value = self.value.merge({custom_messages: messages.presence || {}}) 105 | rescue Psych::SyntaxError => e 106 | self.value = self.value.merge({custom_messages: messages}) 107 | end 108 | 109 | def custom_message_keys_are_available 110 | return if errors.present? 111 | 112 | en_translation_hash = self.class.flatten_hash(MessageCustomize::Locale.available_messages('en')) 113 | custom_message_keys = 114 | custom_messages.values.each_with_object([]){|val, ar| 115 | ar.concat(self.class.flatten_hash(val).keys) 116 | }.uniq 117 | 118 | unused_keys = custom_message_keys.reject{|k| en_translation_hash.keys.include?(:"#{k}")} 119 | unusable_type_of_keys = (custom_message_keys - unused_keys).reject{|k| en_translation_hash[:"#{k}"].is_a?(String)} 120 | 121 | if unused_keys.present? 122 | errors.add(:base, "#{l(:error_unused_keys)} keys: [#{unused_keys.join(', ')}]") 123 | end 124 | if unusable_type_of_keys.present? 125 | errors.add(:base, "#{l(:error_unusable_type_of_keys)} keys: [#{unusable_type_of_keys.join(', ')}]") 126 | end 127 | end 128 | 129 | def custom_message_languages_are_available 130 | return if errors.present? 131 | 132 | unavailable_languages = 133 | custom_messages.keys.compact.reject do |language| 134 | MessageCustomize::Locale.available_locales.include?(language.to_sym) 135 | end 136 | if unavailable_languages.present? 137 | errors.add(:base, l(:error_unavailable_languages) + " [#{unavailable_languages.join(', ')}]") 138 | end 139 | end 140 | 141 | def convertible_to_yaml 142 | raw_messages = raw_custom_messages 143 | if raw_messages.present? && !raw_messages.is_a?(Hash) 144 | begin 145 | YAML.load("#{raw_messages}") 146 | errors.add(:base, l(:error_invalid_yaml_format)) 147 | rescue Psych::SyntaxError => e 148 | errors.add(:base, "#{l(:error_invalid_yaml_format)} #{e.message}") 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /app/views/custom_message_settings/_messages.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= select_tag 'select-key', available_message_options(@setting, lang), id: 'key-selector' %> 3 | <%= sprite_icon('help', l(:text_description_of_search_box)) %> 4 |
5 |
6 |
7 | <%= normal_mode_input_fields(@setting, lang) %> 8 |
9 | 10 | <%= javascript_tag do %> 11 | $(document).ready(function() { 12 | setSelect2(); 13 | }); 14 | 15 | // Fix a problem with focus not working in Redmine 5.0 or later. 16 | // issue: https://github.com/select2/select2/issues/5993 17 | $(document).on('select2:open', function() { 18 | document.querySelector('.select2-search__field').focus(); 19 | }); 20 | 21 | var currentSearchKeyword; 22 | $('#key-selector').on('select2:select', function (e) { 23 | var key = e.params.data.id; 24 | var val = e.params.data.text; 25 | AddMessageInputField(key, val); 26 | }).on('select2:closing', function (e) { 27 | currentSearchKeyword = $('.select2-search__field')[0].value; 28 | }).on('select2:open', function (e) { 29 | setTimeout(function() { 30 | if (currentSearchKeyword){ 31 | $('.select2-search__field').val(currentSearchKeyword).trigger('input'); 32 | } 33 | }, 0); 34 | }); 35 | 36 | function AddMessageInputField(key, val){ 37 | if($('input[name="settings[custom_messages[' + key + ']]"]').length === 0){ 38 | $('

').prependTo($('#edit-custom-messages .tabular')); 39 | $('
').appendTo($('#edit-custom-messages .tabular p:first')); 40 | $('').attr({ 41 | type: 'text', 42 | value: val.replace(/.*: /, ''), 43 | name: 'settings[custom_messages][' + key + ']' 44 | }).appendTo($('#edit-custom-messages .tabular p:first')); 45 | $('').attr({ 46 | class: 'icon clear-key-link', 47 | href: '#', 48 | onclick: '$(this).closest("p").remove(); return false;' 49 | }).html('<%= sprite_icon('del') %>').appendTo($('#edit-custom-messages .tabular p:first')); 50 | $('#key-selector').val('').change(); 51 | $('#key-selector option[value="' + key + '"]').prop("disabled", true).change(); 52 | setSelect2(); 53 | } 54 | } 55 | 56 | function setSelect2(){ 57 | $("#key-selector").select2({ 58 | width: '100%', 59 | placeholder: '<%= l(:text_placeholder_choose_key) %>', 60 | templateResult: function(option) { 61 | return $('' + option.id + ': ' + option.text.replace(/.*: /, '') + ''); 62 | } 63 | }); 64 | } 65 | <% end %> -------------------------------------------------------------------------------- /app/views/custom_message_settings/_normal_tab.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag custom_message_settings_path, method: :post do %> 2 | <%= hidden_field_tag :tab, 'normal' %> 3 | 4 | <%= select_tag 'lang', options_for_select(languages_options, @lang), include_blank: true, data: {remote: true, url: url_for(action: :edit)}, onchange: '$("#ajax-indicator").show();' %> 5 |
6 | 7 |
8 | <%= render 'custom_message_settings/messages', lang: @lang %> 9 |
10 | <%= submit_tag l(:button_save) %> 11 | <% end %> -------------------------------------------------------------------------------- /app/views/custom_message_settings/_yaml_tab.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag custom_message_settings_path, method: :post do %> 2 | <%= hidden_field_tag :tab, 'yaml' %> 3 |
4 |

5 | <%= l(:text_help_for_input_format) %>
6 | <%= l(:text_for_your_reference, open_default_messages_window_link(@lang)).html_safe %> 7 |

8 |

<%= l(:text_placeholder_template_case) %>

9 |
<%= l(:text_placeholder_yaml_template) %>
10 |
11 | <%= text_area_tag 'settings[custom_messages_yaml]', @setting.custom_messages_to_yaml, rows: '30', style: 'width: 100%', placeholder: l(:text_placeholder_yaml_template) %> 12 | <%= submit_tag l(:button_save) %> 13 | <% end %> -------------------------------------------------------------------------------- /app/views/custom_message_settings/default_messages.html.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag('default_messages', plugin: 'redmine_message_customize') %> 2 |

<%= l(:label_default_messages) %>(<%= "config/locales/#{@lang}.yml" %>)

3 | 4 | 5 | <%= select_tag 'lang', options_for_select(languages_options, @lang), onchange: "window.location.replace('#{default_messages_custom_message_settings_path}?lang=' + this.value)" %> 6 | 7 | <% if @file_path.exist? %> 8 |
9 | 10 | 11 | <% line_num = 1 %> 12 | <% syntax_highlight_lines(@file_path.basename.to_s, Redmine::CodesetUtil.to_utf8_by_setting(File.read(@file_path))).each do |line| %> 13 | 14 | 17 | 20 | 21 | <% line_num += 1 %> 22 | <% end %> 23 | 24 |
15 | <%= line_num %> 16 | 18 |
<%= line.html_safe %>
19 |
25 |
26 | <% end %> -------------------------------------------------------------------------------- /app/views/custom_message_settings/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag('custom_messages', plugin: 'redmine_message_customize') %> 2 | <%= stylesheet_link_tag('select2.min', plugin: 'redmine_message_customize') %> 3 | <%= javascript_include_tag('select2.min', plugin: 'redmine_message_customize') %> 4 | <% unless @setting.enabled? %> 5 |
6 | <%= l(:text_disabled_customize) %> 7 |
8 | <% end %> 9 | 10 |
11 | <%= link_to sprite_icon('settings', (@setting.enabled? ? l(:label_disable_customize) : l(:label_enable_customize))), toggle_enabled_custom_message_settings_path, method: :post, class: 'icon' %> / 12 | <%= open_default_messages_window_link(@lang) %> 13 |
14 | 15 |

<%= l(:label_custom_messages) %>

16 | 17 | <% if @setting.errors.any? %> 18 | <%= error_messages_for(@setting) %> 19 | <% end %> 20 | 21 | <%= render_tabs (@setting.errors.any? ? [] : [{name: 'normal', partial: 'normal_tab', label: 'label_normal_tab'}]) + [{name: 'yaml', partial: 'yaml_tab', label: 'label_yaml_tab'}] %> -------------------------------------------------------------------------------- /app/views/custom_message_settings/edit.js.erb: -------------------------------------------------------------------------------- 1 | $('#edit-custom-messages').html("<%= j (render 'custom_message_settings/messages', lang: @lang) %>"); 2 | $('#default-messages-link').replaceWith('<%= open_default_messages_window_link(@lang) %>') 3 | setSelect2(); 4 | $('#ajax-indicator').hide(); -------------------------------------------------------------------------------- /assets/javascripts/select2.min.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | !function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,b;function w(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&b.test(e[s])&&(e[s]=e[s].replace(b,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('
    ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):ithis.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
      '),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.isDisabled()){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('
    • ×
    • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
    • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1