interpret 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +2 -0
- data/app/controllers/interpret/base_controller.rb +10 -14
- data/app/controllers/interpret/missing_translations_controller.rb +41 -10
- data/app/controllers/interpret/search_controller.rb +15 -3
- data/app/controllers/interpret/tools_controller.rb +2 -2
- data/app/controllers/interpret/translations_controller.rb +15 -12
- data/app/models/interpret/ability.rb +9 -0
- data/app/models/interpret/translation.rb +38 -3
- data/app/views/interpret/missing_translations/blank.html.erb +35 -0
- data/app/views/interpret/missing_translations/index.html.erb +25 -14
- data/app/views/interpret/missing_translations/stale.html.erb +43 -0
- data/app/views/interpret/missing_translations/unused.html.erb +27 -0
- data/app/views/interpret/tools/index.html.erb +15 -25
- data/app/views/interpret/translations/_listing.html.erb +3 -15
- data/app/views/interpret/translations/index.html.erb +3 -0
- data/app/views/layouts/interpret.html.erb +26 -6
- data/config/environment.rb +0 -0
- data/config/routes.rb +4 -1
- data/interpret.gemspec +4 -1
- data/lib/generators/interpret/templates/migration.rb +1 -1
- data/lib/interpret.rb +23 -2
- data/lib/interpret/controller_filter.rb +0 -10
- data/lib/interpret/helpers.rb +2 -2
- data/lib/interpret/version.rb +1 -1
- data/public/javascripts/interpret.js +0 -3
- data/public/stylesheets/interpret_style.css +0 -6
- data/spec/integration/missing_translations_spec.rb +61 -0
- data/spec/integration/search_spec.rb +88 -0
- data/spec/integration/stale_translations_spec.rb +28 -0
- data/spec/integration/tools_spec.rb +86 -0
- data/spec/integration/translations_spec.rb +26 -0
- data/spec/models/translation_spec.rb +58 -25
- data/spec/observers/expiration_observer_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -15
- data/spec/support/selenium_db_hack.rb +19 -0
- data/spec/support/utils.rb +89 -0
- data/test_app/Gemfile +1 -1
- data/test_app/app/controllers/application_controller.rb +2 -4
- data/test_app/app/models/interpret_ability.rb +7 -0
- data/test_app/app/models/user.rb +0 -3
- data/test_app/app/views/layouts/application.html.erb +4 -7
- data/test_app/config/initializers/interpret.rb +2 -2
- data/test_app/config/initializers/rack_patch.rb +13 -0
- data/test_app/config/locales/es.yml +1 -1
- data/test_app/db/migrate/20110219173536_create_users.rb +0 -2
- data/test_app/db/migrate/{20110219143622_interpret_create_translations.rb → 20111021100344_interpret_create_translations.rb} +1 -1
- data/test_app/db/schema.rb +2 -3
- data/test_app/db/seeds.rb +1 -1
- data/test_app/public/javascripts/interpret.js +0 -3
- data/test_app/public/stylesheets/interpret_style.css +0 -6
- metadata +122 -119
- data/spec/database_helpers.rb +0 -15
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
Interpret
|
2
2
|
=========
|
3
|
+
[![Build Status](https://secure.travis-ci.org/rogercampos/interpret.png)](http://travis-ci.org/rogercampos/interpret)
|
4
|
+
|
3
5
|
|
4
6
|
Interpret is a rails 3 engine to help you manage your application
|
5
7
|
translations, also allowing you to have editable contents in live. In order to
|
@@ -1,14 +1,16 @@
|
|
1
1
|
class Interpret::BaseController < eval(Interpret.parent_controller.classify)
|
2
2
|
before_filter :set_locale
|
3
|
-
before_filter :
|
3
|
+
before_filter { authorize! :use, :interpret }
|
4
|
+
before_filter :check_authorized_language
|
4
5
|
layout 'interpret'
|
5
6
|
|
6
7
|
protected
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def current_interpret_user
|
9
|
+
@current_interpret_user ||= eval(Interpret.current_user)
|
10
|
+
end
|
11
|
+
|
12
|
+
def current_ability
|
13
|
+
@current_ability ||= Interpret.ability.new(current_interpret_user)
|
12
14
|
end
|
13
15
|
|
14
16
|
private
|
@@ -16,14 +18,8 @@ private
|
|
16
18
|
I18n.locale = params[:locale] if params[:locale]
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
|
21
|
-
@interpret_user = eval(Interpret.current_user)
|
22
|
-
@interpret_admin = true
|
23
|
-
if Interpret.admin && @interpret_user.respond_to?(Interpret.admin)
|
24
|
-
@interpret_admin = @interpret_user.send(Interpret.admin)
|
25
|
-
end
|
26
|
-
end
|
21
|
+
def check_authorized_language
|
22
|
+
authorize! :use, :"interpret_in_#{I18n.locale}"
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
@@ -1,15 +1,46 @@
|
|
1
1
|
class Interpret::MissingTranslationsController < Interpret::BaseController
|
2
2
|
def index
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
authorize! :read, :missing_translations
|
4
|
+
return if I18n.locale == I18n.default_locale
|
5
|
+
|
6
|
+
case ActiveRecord::Base.connection.adapter_name
|
7
|
+
when "Mysql2"
|
8
|
+
res = ActiveRecord::Base.connection.execute("select t.id from translations t where t.locale ='#{I18n.default_locale}' and (select count(*) from translations t2 where t2.key = t.key and t2.locale ='#{I18n.locale}') = 0")
|
9
|
+
|
10
|
+
when "SQLite"
|
11
|
+
res = ActiveRecord::Base.connection.execute("select t.id from translations t where t.locale ='#{I18n.default_locale}' and (select count(*) from translations t2 where t2.key = t.key and t2.locale ='#{I18n.locale}') = 0")
|
12
|
+
|
13
|
+
else
|
14
|
+
raise NotImplementedError, "database adapter not supported"
|
13
15
|
end
|
16
|
+
|
17
|
+
ids = res.map{|x| x.first}
|
18
|
+
translations = Interpret::Translation.allowed.where(:id => ids).order("translations.key ASC").all
|
19
|
+
translations = translations.select{|x| x.value.present?}
|
20
|
+
@missing_translations = translations.map{|x| {:ref_value => x.value, :key => x.key, :source => x}}
|
21
|
+
end
|
22
|
+
|
23
|
+
def blank
|
24
|
+
authorize! :read, :blank_translations
|
25
|
+
@blank_translations = Interpret::Translation.allowed.locale(I18n.locale).where(:value => "--- \"\"\n")
|
26
|
+
@ref_translations = Interpret::Translation.allowed.locale(I18n.default_locale).where(:key => @blank_translations.map{|x| x.key})
|
27
|
+
|
28
|
+
@blank_translations.map do |x|
|
29
|
+
foo = @ref_translations.detect{|y| x.key == y.key}
|
30
|
+
[x, foo ? foo.value : nil]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def unused
|
35
|
+
authorize! :read, :unused_translations
|
36
|
+
used_keys = Interpret::Translation.allowed.locale(I18n.default_locale).all.map{|x| x.key}
|
37
|
+
@unused_translations = Interpret::Translation.allowed.locale(I18n.locale).where("translations.key NOT IN (?)", used_keys)
|
38
|
+
end
|
39
|
+
|
40
|
+
def stale
|
41
|
+
authorize! :read, :stale_translations
|
42
|
+
@stale_translations = Interpret::Translation.allowed.stale.locale(I18n.locale).order("translations.key ASC")
|
43
|
+
refs = Interpret::Translation.locale(I18n.default_locale).where(:key => @stale_translations.map{|x| x.key})
|
44
|
+
@stale_translations.map!{|x| [x, refs.detect{|y| y.key == x.key}]}
|
14
45
|
end
|
15
46
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class Interpret::SearchController < Interpret::BaseController
|
2
|
+
before_filter { authorize! :use, :search }
|
3
|
+
|
2
4
|
def index
|
3
5
|
if request.post?
|
4
6
|
opts = {}
|
@@ -7,10 +9,20 @@ class Interpret::SearchController < Interpret::BaseController
|
|
7
9
|
redirect_to interpret_search_url(opts)
|
8
10
|
else
|
9
11
|
if params[:key].present? || params[:value].present?
|
12
|
+
sanitizer = case ActiveRecord::Base.connection.adapter_name
|
13
|
+
when "SQLite"
|
14
|
+
if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new("1.9")
|
15
|
+
lambda {|x| "%#{x}%"}
|
16
|
+
else
|
17
|
+
lambda {|x| "%#{CGI.escape(x)}%"}
|
18
|
+
end
|
19
|
+
else
|
20
|
+
lambda {|x| "%#{CGI.escape(x)}%"}
|
21
|
+
end
|
10
22
|
t = Interpret::Translation.arel_table
|
11
|
-
search_key = params[:key].present? ? params[:key].split(" ").map{|x|
|
12
|
-
search_value = params[:value].present? ? params[:value].split(" ").map{|x|
|
13
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].matches_all(search_key).or(t[:value].matches_all(search_value))
|
23
|
+
search_key = params[:key].present? ? params[:key].split(" ").map{|x| sanitizer.call(x)} : []
|
24
|
+
search_value = params[:value].present? ? params[:value].split(" ").map{|x| sanitizer.call(x)} : []
|
25
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].matches_all(search_key).or(t[:value].matches_all(search_value))).order("translations.key ASC")
|
14
26
|
end
|
15
27
|
end
|
16
28
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
class Interpret::ToolsController < Interpret::BaseController
|
2
|
-
before_filter :
|
2
|
+
before_filter { authorize! :use, :tools }
|
3
3
|
|
4
4
|
def dump
|
5
5
|
Interpret::Translation.dump
|
@@ -16,7 +16,7 @@ class Interpret::ToolsController < Interpret::BaseController
|
|
16
16
|
hash = Interpret::Translation.export(translations)
|
17
17
|
text = hash.ya2yaml
|
18
18
|
|
19
|
-
send_data text[5..text.length], :filename => "#{I18n.locale}.yml"
|
19
|
+
send_data text[5..text.length], :filename => "#{I18n.locale}.yml", :type => "text/plain", :disposition => "attachment"
|
20
20
|
end
|
21
21
|
|
22
22
|
def run_update
|
@@ -1,28 +1,27 @@
|
|
1
1
|
class Interpret::TranslationsController < Interpret::BaseController
|
2
2
|
before_filter :get_tree, :only => :index
|
3
|
+
authorize_resource :class => "Interpret::Translation"
|
3
4
|
|
4
5
|
def index
|
5
6
|
key = params[:key]
|
6
7
|
t = Interpret::Translation.arel_table
|
7
8
|
if key
|
8
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].matches("#{CGI.escape(key)}.%"))
|
9
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].matches("#{CGI.escape(key)}.%")).order("translations.key ASC")
|
9
10
|
if I18n.locale != I18n.default_locale
|
10
|
-
@references = Interpret::Translation.locale(I18n.default_locale).where(t[:key].matches("#{CGI.escape(key)}.%"))
|
11
|
+
@references = Interpret::Translation.allowed.locale(I18n.default_locale).where(t[:key].matches("#{CGI.escape(key)}.%")).order("translations.key ASC")
|
11
12
|
end
|
12
13
|
else
|
13
|
-
@translations = Interpret::Translation.locale(I18n.locale).where(t[:key].does_not_match("%.%"))
|
14
|
+
@translations = Interpret::Translation.allowed.locale(I18n.locale).where(t[:key].does_not_match("%.%")).order("translations.key ASC")
|
14
15
|
if I18n.locale != I18n.default_locale
|
15
|
-
@references = Interpret::Translation.locale(I18n.default_locale).where(t[:key].does_not_match("%.%"))
|
16
|
+
@references = Interpret::Translation.allowed.locale(I18n.default_locale).where(t[:key].does_not_match("%.%")).order("translations.key ASC")
|
16
17
|
end
|
17
18
|
end
|
18
|
-
if @interpret_user
|
19
|
-
@translations = @translations.where(:protected => false) if !@interpret_admin
|
20
|
-
@references = @references.where(:protected => false) if @references && !@interpret_admin
|
21
|
-
end
|
22
19
|
|
23
20
|
# not show translations inside nested folders, \w avoids dots
|
24
21
|
@translations = @translations.select{|x| x.key =~ /#{key}\.\w+$/} if key
|
25
22
|
@references = @references.select{|x| x.key =~ /#{key}\.\w+$/} if key && @references
|
23
|
+
|
24
|
+
@total_keys_number = Interpret::Translation.locale(I18n.locale).count
|
26
25
|
end
|
27
26
|
|
28
27
|
def edit
|
@@ -30,10 +29,6 @@ class Interpret::TranslationsController < Interpret::BaseController
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def update
|
33
|
-
if @interpret_user && !@interpret_admin && params[:interpret_translation].has_key?(:protected)
|
34
|
-
head :error
|
35
|
-
return
|
36
|
-
end
|
37
32
|
@translation = Interpret::Translation.find(params[:id])
|
38
33
|
old_value = @translation.value
|
39
34
|
|
@@ -67,6 +62,14 @@ class Interpret::TranslationsController < Interpret::BaseController
|
|
67
62
|
end
|
68
63
|
end
|
69
64
|
|
65
|
+
def destroy
|
66
|
+
@translation = Interpret::Translation.find(params[:id])
|
67
|
+
|
68
|
+
@translation.destroy
|
69
|
+
flash[:notice] = "Translation #{@translation.key} destroyed."
|
70
|
+
redirect_to request.env["HTTP_REFERER"]
|
71
|
+
end
|
72
|
+
|
70
73
|
def live_edit
|
71
74
|
blobs = params[:key].split(".")
|
72
75
|
locale = blobs.first
|
@@ -4,7 +4,37 @@ module Interpret
|
|
4
4
|
default_scope order('locale ASC')
|
5
5
|
validates_uniqueness_of :key, :scope => :locale
|
6
6
|
|
7
|
+
after_update :set_stale
|
8
|
+
|
9
|
+
scope :stale, where(:stale => true)
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# If this translations is in the main language, mark this translation in
|
14
|
+
# other languages as stale, so the translators know that they must change
|
15
|
+
# it.
|
16
|
+
def set_stale
|
17
|
+
return unless locale == I18n.default_locale.to_s
|
18
|
+
|
19
|
+
Translation.where(:key => key).where(Translation.arel_table[:locale].not_eq(locale)).update_all({:stale => true})
|
20
|
+
end
|
21
|
+
|
7
22
|
class << self
|
23
|
+
|
24
|
+
def allowed
|
25
|
+
s = order("")
|
26
|
+
if Interpret.wild_blacklist.any?
|
27
|
+
black_keys = Interpret.wild_blacklist.map{|x| "#{CGI.escape(x)}%"}
|
28
|
+
s = s.where(arel_table[:key].does_not_match_all(black_keys))
|
29
|
+
end
|
30
|
+
if Interpret.fixed_blacklist.any?
|
31
|
+
black_keys = Interpret.fixed_blacklist.map{|x| "#{CGI.escape(x)}"}
|
32
|
+
s = s.where(arel_table[:key].does_not_match_all(black_keys))
|
33
|
+
end
|
34
|
+
s
|
35
|
+
end
|
36
|
+
|
37
|
+
|
8
38
|
# Generates a hash representing the tree structure of the translations
|
9
39
|
# for the given locale. It includes only "folders" in the sense of
|
10
40
|
# locale keys that includes some real translations, or other keys.
|
@@ -40,10 +70,9 @@ module Interpret
|
|
40
70
|
|
41
71
|
# Import the contents of the given .yml locale file into the database
|
42
72
|
# backend. If a given key already exists in database, it will be
|
43
|
-
# overwritten, otherwise it won't be touched. This means that
|
73
|
+
# overwritten, otherwise it won't be touched. This means that it won't
|
44
74
|
# delete any existing translation, it only overwrites the ones you give
|
45
75
|
# in the file.
|
46
|
-
# If the given file has new translations, these will be ignored.
|
47
76
|
#
|
48
77
|
# The language will be obtained from the first unique key of the yml
|
49
78
|
# file.
|
@@ -53,12 +82,18 @@ module Interpret
|
|
53
82
|
|
54
83
|
lang = hash.keys.first
|
55
84
|
|
85
|
+
unless lang.to_s == I18n.locale.to_s
|
86
|
+
raise ArgumentError, "the language doesn't match"
|
87
|
+
end
|
88
|
+
|
56
89
|
records = parse_hash(hash.first[1], lang)
|
57
90
|
transaction do
|
58
91
|
records.each do |x|
|
59
92
|
if tr = locale(lang).find_by_key(x.key)
|
60
93
|
tr.value = x.value
|
61
|
-
tr.save
|
94
|
+
tr.save!
|
95
|
+
else
|
96
|
+
x.save!
|
62
97
|
end
|
63
98
|
end
|
64
99
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<% if @blank_translations.empty? %>
|
2
|
+
<p>There are no blank translations</p>
|
3
|
+
<% else %>
|
4
|
+
<p>There are <%= @blank_translations.size %> blank translations in this
|
5
|
+
language [ <%= I18n.locale %> ]. These are translations that exists but are
|
6
|
+
empty strings, so I show you them in case you want to take some action.</p>
|
7
|
+
<table>
|
8
|
+
<thead>
|
9
|
+
<tr class="header">
|
10
|
+
<th>Key</th>
|
11
|
+
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
12
|
+
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
13
|
+
<th>Delete</th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<% @blank_translations.each do |trans, ref| %>
|
17
|
+
<tr>
|
18
|
+
<td><%= trans.key %></td>
|
19
|
+
<td><%= ref || "It's empty" %></td>
|
20
|
+
<td>
|
21
|
+
<%= form_for Interpret::Translation.new, :url => interpret_translations_path do |f| %>
|
22
|
+
<%= f.hidden_field "locale", :value => I18n.locale %>
|
23
|
+
<%= f.hidden_field "key", :value => trans.key %>
|
24
|
+
<%= f.text_area :value, :rows => 4, :cols => 60 %>
|
25
|
+
<%= submit_tag "Create" %>
|
26
|
+
<% end %>
|
27
|
+
</td>
|
28
|
+
<td>
|
29
|
+
<%= link_to "Destroy it", trans, :method => :delete, :confirm => "Are you sure?" %>
|
30
|
+
</td>
|
31
|
+
</tr>
|
32
|
+
<% end %>
|
33
|
+
</table>
|
34
|
+
<% end %>
|
35
|
+
|
@@ -7,27 +7,38 @@
|
|
7
7
|
create them below and you can see the translation key and also the original
|
8
8
|
content in the reference language [ <%= I18n.default_locale %> ]
|
9
9
|
</p>
|
10
|
-
<table>
|
10
|
+
<table id='missing_translations'>
|
11
11
|
<thead>
|
12
12
|
<tr class="header">
|
13
13
|
<th>Key</th>
|
14
14
|
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
15
15
|
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
16
|
+
<% if can? :destroy, Interpret::Translation %>
|
17
|
+
<th>Delete</th>
|
18
|
+
<% end %>
|
16
19
|
</tr>
|
17
20
|
</thead>
|
18
|
-
|
19
|
-
|
20
|
-
<
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
<%=
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
<tbody>
|
22
|
+
<% @missing_translations.each do |trans| %>
|
23
|
+
<tr>
|
24
|
+
<td><%= trans[:key] %></td>
|
25
|
+
<td><%= trans[:ref_value] %></td>
|
26
|
+
<td>
|
27
|
+
<%= form_for Interpret::Translation.new, :url => interpret_translations_path do |f| %>
|
28
|
+
<%= f.hidden_field "locale", :value => I18n.locale %>
|
29
|
+
<%= f.hidden_field "key", :value => trans[:key] %>
|
30
|
+
<%= f.text_area :value, :rows => 4, :cols => 60 %>
|
31
|
+
<%= submit_tag "Create" %>
|
32
|
+
<% end %>
|
33
|
+
</td>
|
34
|
+
<% if can? :destroy, trans[:source] %>
|
35
|
+
<td>
|
36
|
+
<%= link_to "Destroy", trans[:source], :method => :delete, :confirm => "Are you sure?" %>
|
37
|
+
the original [ <%= I18n.default_locale %> ] translation.
|
38
|
+
</td>
|
28
39
|
<% end %>
|
29
|
-
</
|
30
|
-
|
31
|
-
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</tbody>
|
32
43
|
</table>
|
33
44
|
<% end %>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<% if I18n.locale == I18n.default_locale %>
|
2
|
+
<p>There can't be stale translations for the main language</p>
|
3
|
+
|
4
|
+
<% elsif @stale_translations.empty? %>
|
5
|
+
<p>There are no stale translations</p>
|
6
|
+
|
7
|
+
<% else %>
|
8
|
+
<p>There are <%= @stale_translations.size %> stale translations in [<%= I18n.locale %>].</p>
|
9
|
+
<p>
|
10
|
+
They have been modified in the master language (<%= I18n.default_locale %>)
|
11
|
+
so probably you want to change them in the current languge too.
|
12
|
+
Once you think your new adaptation is correct click on "Mark as OK" button and that translation will no longer appear as stale.
|
13
|
+
</p>
|
14
|
+
<table id='stale_translations'>
|
15
|
+
<thead>
|
16
|
+
<tr class="header">
|
17
|
+
<th>Key</th>
|
18
|
+
<th>Value in [ <%= I18n.default_locale %> ]</th>
|
19
|
+
<th>Your translation to [ <%= I18n.locale %> ]</th>
|
20
|
+
<th>Actions</th>
|
21
|
+
</tr>
|
22
|
+
</thead>
|
23
|
+
<% @stale_translations.each do |trans, ref| %>
|
24
|
+
<tr>
|
25
|
+
<td class='key'><%= trans.key %></td>
|
26
|
+
<td><%= ref.value %></td>
|
27
|
+
<td class='content' id='translation_<%= trans.id %>'>
|
28
|
+
<%= best_in_place trans, :value,
|
29
|
+
:type => :textarea,
|
30
|
+
:path => interpret_translation_path(trans),
|
31
|
+
:activator => "#translation_#{trans.id}",
|
32
|
+
:sanitize => false %>
|
33
|
+
</td>
|
34
|
+
<td>
|
35
|
+
<%= form_for trans, :url => interpret_translation_path(trans) do |f| %>
|
36
|
+
<%= f.hidden_field :stale, :value => false %>
|
37
|
+
<%= submit_tag "Mark as OK" %>
|
38
|
+
<% end %>
|
39
|
+
</td>
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</table>
|
43
|
+
<% end %>
|