tr8n 3.1.6 → 3.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/{local/tr8n_server/app/assets/stylesheets/admin.css → app/assets/stylesheets/tr8n/admin.css.scss} +0 -0
- data/app/assets/stylesheets/tr8n/components.css.scss +211 -0
- data/app/assets/stylesheets/tr8n/layout.css.scss +143 -0
- data/app/models/tr8n/language_rule.rb +31 -29
- data/app/models/tr8n/sync_log.rb +114 -19
- data/app/models/tr8n/translation.rb +43 -40
- data/app/models/tr8n/translation_key.rb +48 -18
- data/config/routes.rb +1 -1
- data/lib/generators/tr8n/templates/config/tr8n/config.yml +2 -2
- data/lib/generators/tr8n/templates/layouts/tr8n.html.erb +4 -1
- data/lib/generators/tr8n/templates/layouts/tr8n_admin.html.erb +5 -3
- data/lib/generators/tr8n/tr8n_generator.rb +1 -1
- data/lib/tasks/tr8n.rake +4 -2
- data/lib/tr8n/config.rb +1 -1
- data/lib/tr8n/version.rb +1 -1
- data/local/tr8n_server/app/assets/stylesheets/admin.css.scss +200 -0
- data/local/tr8n_server/app/assets/stylesheets/{application.css → application.css.scss} +0 -0
- metadata +43 -42
- data/app/controllers/tr8n/api/v1/sync_controller.rb +0 -30
- data/app/controllers/tr8n/api/v1/translator_controller.rb +0 -34
@@ -90,7 +90,7 @@ class Tr8n::Translation < ActiveRecord::Base
|
|
90
90
|
return nil if super_rules == nil
|
91
91
|
return nil unless super_rules.class.name == 'Array'
|
92
92
|
return nil if super_rules.size == 0
|
93
|
-
|
93
|
+
|
94
94
|
@loaded_rules ||= begin
|
95
95
|
rulz = []
|
96
96
|
super_rules.each do |rule|
|
@@ -118,43 +118,6 @@ class Tr8n::Translation < ActiveRecord::Base
|
|
118
118
|
end
|
119
119
|
end
|
120
120
|
|
121
|
-
# generates the hash without rule ids, but with full definitions
|
122
|
-
def rules_api_hash
|
123
|
-
@rules_api_hash ||= (rules || []).collect{|rule_hash| rule_hash[:rule].to_api_hash.merge(:token => rule_hash[:token])}
|
124
|
-
end
|
125
|
-
|
126
|
-
# serilaize translation to API hash to be used for synchronization
|
127
|
-
def to_api_hash
|
128
|
-
{:locale => language.locale, :label => label, :rank => rank, :rules => rules_api_hash}
|
129
|
-
end
|
130
|
-
|
131
|
-
# create translation from API hash for a specific key
|
132
|
-
def self.create_from_api_hash(tkey, translator, hash, opts = {})
|
133
|
-
return if hash[:label].blank? # don't add empty translations
|
134
|
-
lang = Tr8n::Language.for(hash[:locale])
|
135
|
-
return unless lang # don't add translations for an unsupported language
|
136
|
-
|
137
|
-
tkey.translations.each do |trn|
|
138
|
-
# if an identical translation exists, don't add it
|
139
|
-
return if trn.to_api_hash == hash
|
140
|
-
end
|
141
|
-
|
142
|
-
# generate rules for the translation
|
143
|
-
rules = nil
|
144
|
-
|
145
|
-
if hash[:rules].any?
|
146
|
-
hash[:rules].each do |rule_hash|
|
147
|
-
return unless rule_hash[:token] and rule_hash[:type] and rule_hash[:definition]
|
148
|
-
|
149
|
-
rule = Tr8n::LanguageRule.for_definition(lang, translator, rule_hash[:type], rule_hash[:definition], opts)
|
150
|
-
return unless rule # if the rule has not been created, we should not even add the translation
|
151
|
-
rules << {:token => rule_hash[:token], :rule_id => rule.id}
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
tkey.add_translation(hash[:label], rules, lang, translator)
|
156
|
-
end
|
157
|
-
|
158
121
|
# deprecated - api_hash should be used instead
|
159
122
|
def rules_definitions
|
160
123
|
return nil if rules.nil? or rules.empty?
|
@@ -257,9 +220,49 @@ class Tr8n::Translation < ActiveRecord::Base
|
|
257
220
|
end
|
258
221
|
|
259
222
|
###############################################################
|
260
|
-
##
|
223
|
+
## Synchronization Methods
|
224
|
+
###############################################################
|
225
|
+
# generates the hash without rule ids, but with full definitions
|
226
|
+
def rules_sync_hash
|
227
|
+
@rules_sync_hash ||= (rules || []).collect{|rule| rule[:rule].to_sync_hash(rule[:token])}
|
228
|
+
end
|
229
|
+
|
230
|
+
# serilaize translation to API hash to be used for synchronization
|
231
|
+
def to_sync_hash(include_translator = true)
|
232
|
+
hash = {"locale" => language.locale, "label" => label, "rank" => rank, "rules" => rules_sync_hash}
|
233
|
+
hash["translator_id"] = translator.remote_id if include_translator and translator and translator.remote_id
|
234
|
+
hash
|
235
|
+
end
|
236
|
+
|
237
|
+
# create translation from API hash for a specific key
|
238
|
+
def self.create_from_sync_hash(tkey, translator, hash, opts = {})
|
239
|
+
# don't add empty translations
|
240
|
+
return if hash["label"].blank?
|
241
|
+
|
242
|
+
lang = Tr8n::Language.for(hash["locale"])
|
243
|
+
# don't add translations for an unsupported language
|
244
|
+
return unless lang
|
245
|
+
|
246
|
+
# generate rules for the translation
|
247
|
+
rules = []
|
248
|
+
|
249
|
+
if hash["rules"] and hash["rules"].any?
|
250
|
+
hash["rules"].each do |rule_hash|
|
251
|
+
rule = Tr8n::LanguageRule.create_from_sync_hash(lang, translator, rule_hash, opts)
|
252
|
+
pp rule
|
253
|
+
|
254
|
+
return unless rule # if the rule has not been created, we should not even add the translation
|
255
|
+
rules << {:token => rule_hash["token"], :rule_id => rule.id}
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
rules = nil if rules.empty?
|
260
|
+
tkey.add_translation(hash["label"], rules, lang, translator)
|
261
|
+
end
|
262
|
+
|
263
|
+
###############################################################
|
264
|
+
## Search Methods
|
261
265
|
###############################################################
|
262
|
-
|
263
266
|
def self.filter_status_options
|
264
267
|
[["all translations", "all"],
|
265
268
|
["accepted translations", "accepted"],
|
@@ -68,6 +68,7 @@ class Tr8n::TranslationKey < ActiveRecord::Base
|
|
68
68
|
:locale => locale,
|
69
69
|
:level => level,
|
70
70
|
:admin => Tr8n::Config.block_options[:admin])
|
71
|
+
|
71
72
|
unless options[:source].blank?
|
72
73
|
# at the time of creation - mark the first source of the key
|
73
74
|
Tr8n::TranslationKeySource.find_or_create(new_tkey, Tr8n::TranslationSource.find_or_create(options[:source], options[:url]))
|
@@ -218,8 +219,10 @@ class Tr8n::TranslationKey < ActiveRecord::Base
|
|
218
219
|
|
219
220
|
def add_translation(label, rules = nil, language = Tr8n::Config.current_language, translator = Tr8n::Config.current_translator)
|
220
221
|
raise Tr8n::Exception.new("The sentence contains dirty words") unless language.clean_sentence?(label)
|
221
|
-
|
222
|
-
|
222
|
+
pp rules
|
223
|
+
translation = Tr8n::Translation.create(:translation_key => self, :language => language, :translator => translator, :label => label, :rules => rules)
|
224
|
+
pp translation
|
225
|
+
|
223
226
|
translation.vote!(translator, 1)
|
224
227
|
translation
|
225
228
|
end
|
@@ -524,35 +527,62 @@ class Tr8n::TranslationKey < ActiveRecord::Base
|
|
524
527
|
Tr8n::Cache.delete("translation_key_#{key}")
|
525
528
|
end
|
526
529
|
|
527
|
-
|
530
|
+
###############################################################
|
531
|
+
## Synchronization Methods
|
532
|
+
###############################################################
|
533
|
+
def mark_as_synced!
|
534
|
+
update_attributes(:synced_at => Time.now + 5.seconds)
|
535
|
+
end
|
536
|
+
|
537
|
+
def to_sync_hash(default_translation_hashes = nil)
|
528
538
|
{
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
539
|
+
"key" => self.key,
|
540
|
+
"label" => self.label,
|
541
|
+
"description" => self.description,
|
542
|
+
"locale" => (locale || Tr8n::Config.default_locale),
|
543
|
+
"translations" => default_translation_hashes || translations_for(nil, Tr8n::Config.translation_threshold).collect{|t| t.to_sync_hash}
|
534
544
|
}
|
535
545
|
end
|
536
546
|
|
547
|
+
def transations_sync_hashes
|
548
|
+
@transations_sync_hashes ||= translations.collect{|t| t.to_sync_hash(false)}
|
549
|
+
end
|
550
|
+
|
537
551
|
# create translation key from API hash
|
538
|
-
def self.
|
539
|
-
return if tkey_hash[
|
552
|
+
def self.create_from_sync_hash(tkey_hash, translator, opts = {})
|
553
|
+
return if tkey_hash["key"].blank? or tkey_hash["label"].blank? or tkey_hash["locale"].blank?
|
540
554
|
|
541
|
-
tkey = Tr8n::TranslationKey.find_or_create(tkey_hash[
|
555
|
+
tkey = Tr8n::TranslationKey.find_or_create(tkey_hash["label"], tkey_hash["description"])
|
542
556
|
|
543
557
|
# return unless tkey.key==tkey_hash[:key] # need to warn the user that the key methods don't match
|
544
558
|
|
545
|
-
opts[:force_create] = Tr8n::Config.synchronization_create_rules? if opts[:force_create].nil?
|
559
|
+
# opts[:force_create] = Tr8n::Config.synchronization_create_rules? if opts[:force_create].nil?
|
560
|
+
|
561
|
+
remaining_translations = tkey.transations_sync_hashes.dup
|
562
|
+
# pp :before, remaining_sync_hashes
|
546
563
|
|
547
|
-
|
548
|
-
|
564
|
+
added_trans = []
|
565
|
+
(tkey_hash["translations"] || []).each do |t_hash|
|
566
|
+
# if the translation came from a linked translator, use the translator
|
567
|
+
translation_translator = translator
|
568
|
+
if t_hash["translator_id"]
|
569
|
+
translation_translator = Tr8n::Translator.find_by_id(t_hash["translator_id"])
|
570
|
+
t_hash.delete("translator_id")
|
571
|
+
translation_translator ||= translator
|
572
|
+
end
|
573
|
+
|
574
|
+
remaining_translations.delete(t_hash)
|
575
|
+
next if tkey.transations_sync_hashes.include?(t_hash)
|
576
|
+
trans = Tr8n::Translation.create_from_sync_hash(tkey, translation_translator, t_hash, opts)
|
549
577
|
end
|
550
578
|
|
551
|
-
|
579
|
+
# need to send back translations that have not been added, but exist in the system
|
580
|
+
# pp :after, remaining_sync_hashes
|
581
|
+
[tkey, remaining_translations]
|
552
582
|
end
|
553
583
|
|
554
584
|
###############################################################
|
555
|
-
## Feature
|
585
|
+
## Feature Methods
|
556
586
|
###############################################################
|
557
587
|
|
558
588
|
def self.title
|
@@ -580,7 +610,7 @@ class Tr8n::TranslationKey < ActiveRecord::Base
|
|
580
610
|
end
|
581
611
|
|
582
612
|
###############################################################
|
583
|
-
## Search
|
613
|
+
## Search Methods
|
584
614
|
###############################################################
|
585
615
|
|
586
616
|
def self.filter_phrase_type_options
|
@@ -641,6 +671,6 @@ class Tr8n::TranslationKey < ActiveRecord::Base
|
|
641
671
|
results = results.where("tr8n_translation_keys.id not in (select tr8n_translation_key_locks.translation_key_id from tr8n_translation_key_locks where tr8n_translation_key_locks.language_id = ? and tr8n_translation_key_locks.locked = ?)", Tr8n::Config.current_language.id, true)
|
642
672
|
end
|
643
673
|
|
644
|
-
results
|
674
|
+
results.order("created_at desc")
|
645
675
|
end
|
646
676
|
end
|
data/config/routes.rb
CHANGED
@@ -265,8 +265,8 @@ defaults:
|
|
265
265
|
#############################################################################
|
266
266
|
synchronization:
|
267
267
|
server: "http://tr8n.net" # alternative, regional locations will be available in the future
|
268
|
-
key: "
|
269
|
-
secret: "
|
268
|
+
key: "YOUR APP KEY" # replace this with your key
|
269
|
+
secret: "YOUR APP SECRET" # replace this with your secret
|
270
270
|
batch_size: 50 # how many kesy to send to the server at a time
|
271
271
|
create_rules: true # force rules creation, or skip translations for rules that don't exist
|
272
272
|
all_languages: false # use only enabled languages, or all languages
|
@@ -11,6 +11,9 @@
|
|
11
11
|
<div class="page_head">
|
12
12
|
<div class="page_fixed">
|
13
13
|
<div class="mrgn_h">
|
14
|
+
<ul class="flt_r horiz_list top_nav">
|
15
|
+
<li><%= tr8n_language_selector_tag(:lightbox=>false) %></li>
|
16
|
+
</ul>
|
14
17
|
<h1 class="logo"><%=tr("Tr8n Translation Engine", "Application title")%></h1>
|
15
18
|
</div>
|
16
19
|
</div>
|
@@ -37,7 +40,7 @@
|
|
37
40
|
<li><%=link_to(tr("Credits"), "/tr8n/help/credits", :class => "quiet")%></li>
|
38
41
|
<li><%=link_to(tr("License"), "/tr8n/help/license", :class => "quiet")%></li>
|
39
42
|
</ul>
|
40
|
-
© Copyright 2010 -
|
43
|
+
© Copyright 2010 - 2012 tr8n.net
|
41
44
|
</div>
|
42
45
|
|
43
46
|
<div style="padding:15px;">
|
@@ -23,8 +23,10 @@
|
|
23
23
|
<div class="page_head">
|
24
24
|
<div class="page_fixed">
|
25
25
|
<div class="mrgn_h">
|
26
|
-
|
27
|
-
|
26
|
+
<ul class="flt_r horiz_list top_nav">
|
27
|
+
<li><%= tr8n_language_selector_tag(:lightbox=>false) %></li>
|
28
|
+
</ul>
|
29
|
+
<h1 class="logo"><%=link_to(tr("Tr8n Translation Engine", "Application title"), "/", :style=>"text-decorations:none;")%></h1>
|
28
30
|
</div>
|
29
31
|
</div>
|
30
32
|
</div>
|
@@ -49,7 +51,7 @@
|
|
49
51
|
<li><%=link_to(tr("Credits"), "/tr8n/help/credits", :class => "quiet")%></li>
|
50
52
|
<li><%=link_to(tr("License"), "/tr8n/help/license", :class => "quiet")%></li>
|
51
53
|
</ul>
|
52
|
-
© Copyright 2010 -
|
54
|
+
© Copyright 2010 - 2012 tr8n.net
|
53
55
|
</div>
|
54
56
|
|
55
57
|
<div style="padding:15px;">
|
@@ -45,7 +45,7 @@ class Tr8nGenerator < Rails::Generators::Base
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def copy_configuration
|
48
|
-
config_source = File.expand_path("#{self.class.source_root}/config
|
48
|
+
config_source = File.expand_path("#{self.class.source_root}/config/tr8n", __FILE__)
|
49
49
|
system "rsync -ruv #{config_source} #{Rails.root}/config"
|
50
50
|
end
|
51
51
|
|
data/lib/tasks/tr8n.rake
CHANGED
@@ -85,8 +85,10 @@ namespace :tr8n do
|
|
85
85
|
Tr8n::IpLocation.import_from_file('config/tr8n/data/ip_locations.csv', :verbose => true)
|
86
86
|
end
|
87
87
|
|
88
|
-
desc "
|
88
|
+
desc "Synchronize translations with tr8n.net"
|
89
89
|
task :sync => :environment do
|
90
|
-
|
90
|
+
opts = {}
|
91
|
+
opts[:force] = true if ENV["force"] == "true"
|
92
|
+
Tr8n::SyncLog.sync(opts)
|
91
93
|
end
|
92
94
|
end
|
data/lib/tr8n/config.rb
CHANGED
@@ -532,7 +532,7 @@ module Tr8n
|
|
532
532
|
|
533
533
|
def self.language_rule_dependencies
|
534
534
|
@language_rule_dependencies ||= begin
|
535
|
-
depts =
|
535
|
+
depts = HashWithIndifferentAccess.new
|
536
536
|
language_rule_classes.each do |cls|
|
537
537
|
if depts[cls.dependency]
|
538
538
|
raise Tr8n::Exception.new("The same dependency key #{cls.dependency} has been registered for multiple rules. This is not allowed.")
|
data/lib/tr8n/version.rb
CHANGED
@@ -0,0 +1,200 @@
|
|
1
|
+
/****************************************/
|
2
|
+
/*** Classes for the admin user page
|
3
|
+
/****************************************/
|
4
|
+
@mixin rounded-corners($radius) {
|
5
|
+
border-radius: $radius;
|
6
|
+
-moz-border-radius: $radius;
|
7
|
+
-webkit-border-radius: $radius;
|
8
|
+
}
|
9
|
+
|
10
|
+
@mixin shadow($info) {
|
11
|
+
box-shadow:$info;
|
12
|
+
-moz-box-shadow:$info;
|
13
|
+
-webkit-box-shadow:$info;
|
14
|
+
}
|
15
|
+
|
16
|
+
@import "layout.css.scss";
|
17
|
+
@import "components.css.scss";
|
18
|
+
|
19
|
+
.pagination {
|
20
|
+
text-align:left !important;
|
21
|
+
}
|
22
|
+
|
23
|
+
.admin_table {
|
24
|
+
width: 100%;
|
25
|
+
font-size: 12px;
|
26
|
+
border: 1px #e5e5e5 solid;
|
27
|
+
border-collapse: collapse;
|
28
|
+
background-color: white;
|
29
|
+
margin-bottom: 10px;
|
30
|
+
}
|
31
|
+
|
32
|
+
.admin_table th {
|
33
|
+
background-color: #efefef;
|
34
|
+
border: none;
|
35
|
+
border: 1px #e5e5e5 solid;
|
36
|
+
font-weight:bold;
|
37
|
+
}
|
38
|
+
|
39
|
+
.admin_table tr:hover {
|
40
|
+
background-color: #eee;
|
41
|
+
}
|
42
|
+
|
43
|
+
.admin_table td {
|
44
|
+
padding: 5px;
|
45
|
+
border: 1px solid #e5e5e5;
|
46
|
+
}
|
47
|
+
|
48
|
+
.admin_table td.numeric,
|
49
|
+
.admin_table th.numeric {
|
50
|
+
text-align: right;
|
51
|
+
}
|
52
|
+
|
53
|
+
.admin_table span.description {
|
54
|
+
color: #aaaaaa;
|
55
|
+
font-weight: normal;
|
56
|
+
font-size: 11px;
|
57
|
+
}
|
58
|
+
|
59
|
+
.admin_table td.key {
|
60
|
+
vertical-align:top;
|
61
|
+
width: 33%;
|
62
|
+
color: #666666;
|
63
|
+
font-weight: bold;
|
64
|
+
text-align: right;
|
65
|
+
padding-left: 10px;
|
66
|
+
}
|
67
|
+
|
68
|
+
.admin_table td.value {
|
69
|
+
vertical-align:top;
|
70
|
+
}
|
71
|
+
|
72
|
+
.admin_table td.value ul {
|
73
|
+
margin-top:0px;
|
74
|
+
margin-left:1.5em;
|
75
|
+
}
|
76
|
+
|
77
|
+
.admin_table td.value li {
|
78
|
+
line-height:1em;
|
79
|
+
margin-top:8px;
|
80
|
+
margin-left:0;
|
81
|
+
text-indent:-0.25em;
|
82
|
+
}
|
83
|
+
|
84
|
+
.admin_table td.value .detail {
|
85
|
+
font-size:11px;
|
86
|
+
color:#666;
|
87
|
+
}
|
88
|
+
|
89
|
+
.admin_table td .action_link {
|
90
|
+
float:right;
|
91
|
+
font-size:11px;
|
92
|
+
}
|
93
|
+
|
94
|
+
.admin_table p.status {
|
95
|
+
font-weight:bold;
|
96
|
+
}
|
97
|
+
|
98
|
+
.admin_table p.closed {
|
99
|
+
color:Red;
|
100
|
+
}
|
101
|
+
|
102
|
+
.admin_table p.claimed {
|
103
|
+
color:Green;
|
104
|
+
}
|
105
|
+
|
106
|
+
.admin_table .obscured {
|
107
|
+
background-color:Black;
|
108
|
+
padding:2px 6px;
|
109
|
+
}
|
110
|
+
|
111
|
+
.admin_table .obscured:hover {
|
112
|
+
background-color:#eee;
|
113
|
+
}
|
114
|
+
|
115
|
+
.admin_actions {
|
116
|
+
width: 100%;
|
117
|
+
background-color: #fffeee;
|
118
|
+
border: 1px #e5e5e5 solid;
|
119
|
+
margin-bottom: 10px;
|
120
|
+
}
|
121
|
+
|
122
|
+
.admin_actions th {
|
123
|
+
border: 1px #e5e5e5 solid;
|
124
|
+
font-weight:bold;
|
125
|
+
}
|
126
|
+
|
127
|
+
.admin_actions td {
|
128
|
+
padding: 5px;
|
129
|
+
}
|
130
|
+
|
131
|
+
.admin_actions td a {
|
132
|
+
text-decoration: none;
|
133
|
+
display:block;
|
134
|
+
display:inline-block;
|
135
|
+
width: 100%;
|
136
|
+
}
|
137
|
+
|
138
|
+
.admin_actions td a:hover {
|
139
|
+
text-decoration: underline;
|
140
|
+
}
|
141
|
+
|
142
|
+
.admin_table .expand_link {
|
143
|
+
float:right;
|
144
|
+
}
|
145
|
+
|
146
|
+
.admin_footer_edit {font-size:11px;margin-left:10px;margin-bottom:10px;font-weight:normal;height:20px;padding:5px;background-color:#feffef;border:solid 1px #e4e3c9;}
|
147
|
+
|
148
|
+
#subtabs {
|
149
|
+
margin-bottom: 1em;
|
150
|
+
}
|
151
|
+
|
152
|
+
#date_container {
|
153
|
+
margin-bottom: 1em;
|
154
|
+
font-weight: bold;
|
155
|
+
}
|
156
|
+
|
157
|
+
#totals {
|
158
|
+
border: 2px solid #ccc;
|
159
|
+
font-size: 24px;
|
160
|
+
font-weight: bold;
|
161
|
+
}
|
162
|
+
|
163
|
+
#totals td, th {
|
164
|
+
padding: 5px;
|
165
|
+
border: 1px solid #999;
|
166
|
+
}
|
167
|
+
|
168
|
+
.hourly.breakdown th {
|
169
|
+
text-align: center;
|
170
|
+
}
|
171
|
+
|
172
|
+
a {
|
173
|
+
text-decoration: none;
|
174
|
+
}
|
175
|
+
|
176
|
+
a:hover {
|
177
|
+
text-decoration: underline;
|
178
|
+
}
|
179
|
+
|
180
|
+
.admin_menu_item {
|
181
|
+
padding-left:15px;
|
182
|
+
padding-bottom:3px;
|
183
|
+
text-align:left;
|
184
|
+
width:90%;
|
185
|
+
border-right:1px solid #ccc;
|
186
|
+
}
|
187
|
+
|
188
|
+
.admin_menu_item_selected {
|
189
|
+
padding-left:15px;
|
190
|
+
text-align:left;
|
191
|
+
width:90%;
|
192
|
+
border-top:1px solid #ccc;
|
193
|
+
border-bottom:1px solid #ccc;
|
194
|
+
border-left:1px solid #ccc;
|
195
|
+
border-right:0px;
|
196
|
+
}
|
197
|
+
|
198
|
+
.admin_menu_item_selected a {
|
199
|
+
color:black;
|
200
|
+
}
|