translate-rails3 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,62 @@
1
+ Translate
2
+ =========
3
+
4
+ This plugin provides a web interface for translating Rails I18n texts (requires Rails 3.0 or higher) from one locale to another. The plugin has been tested only with the simple I18n backend that ships with Rails. I18n texts are read from and written to YAML files under config/locales.
5
+
6
+ To translate to a new locale you need to add a YAML file for that locale that contains the locale as the top key and at least one translation.
7
+
8
+ Please note that there are certain I18n keys that map to Array objects rather than strings and those are currently not dealt with by the translation UI. This means that Rails built in keys such as date.day_names need to be translated manually directly in the YAML file.
9
+
10
+ To get the translation UI to write the YAML files in UTF8 you need to install the ya2yaml gem.
11
+
12
+ The translation UI finds all I18n keys by extracting them from I18n lookups in your application source code. In addition it adds all :en and default locale keys from the I18n backend.
13
+
14
+ - Updated: Each string in the UI now has an "Auto Translate" link which will send the original text to Google Translate and will input the returned translation into the form field for further clean up and review prior to saving.
15
+
16
+
17
+ Rake Tasks
18
+ =========
19
+
20
+ In addition to the web UI this plugin adds the following rake tasks:
21
+
22
+ translate:untranslated
23
+ translate:missing
24
+ translate:remove_obsolete_keys
25
+ translate:merge_keys
26
+ translate:google
27
+ translate:changed
28
+
29
+ The missing task shows you any I18n keys in your code that do not have translations in the YAML file for your default locale, i.e. config/locales/sv.yml.
30
+
31
+ The merge_keys task is supposed to be used in conjunction with Sven Fuch's Rails I18n TextMate bundle (http://github.com/svenfuchs/rails-i18n/tree/master). Texts and keys extracted with the TextMate bundle end up in the temporary file log/translations.yml. When you run the merge_keys rake task the keys are moved over to the corresponding I18n locale file, i.e. config/locales/sv.yml. The merge_keys task also checks for overwrites of existing keys by warning you that one of your extracted keys already exists with a different translation.
32
+
33
+ The google task is used for auto translating from one locale to another using Google Translate.
34
+
35
+ The changed rake task can show you between one YAML file to another which keys have had their texts changed.
36
+
37
+ Installation
38
+ =========
39
+
40
+ Add to your Gemfile:
41
+
42
+ gem 'translate-rails3', :require => 'translate', :group => :development
43
+
44
+ Now visit /translate in your web browser to start translating.
45
+
46
+ Dependencies
47
+ =========
48
+
49
+ - Rails 2.2 or higher
50
+ - The ya2yaml gem if you want your YAML files written in UTF8 encoding.
51
+
52
+ Authors
53
+ =========
54
+
55
+ - Peter Marklund (programming)
56
+ - Joakim Westerlund (web design)
57
+ - Milan Novota (initial Rails 3 support)
58
+ - Roman Shterenzon (Rails 3 cleanup and gem packaging)
59
+
60
+ Many thanks to http://newsdesk.se for sponsoring the development of this plugin.
61
+
62
+ Copyright (c) 2009 Peter Marklund, released under the MIT license
@@ -0,0 +1,33 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc 'Default: run specs.'
4
+ task :default => :spec
5
+
6
+ desc 'Run the specs'
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.rspec_opts = ['--color --format progress']
9
+ end
10
+
11
+ begin
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ gem.name = 'translate-rails3'
15
+ gem.summary = %Q{Newsdesk translate plugin for Rails 3}
16
+ gem.description = <<EOF
17
+ This plugin provides a web interface for translating Rails I18n texts
18
+ (requires Rails 3.0 or higher) from one locale to another.
19
+ The plugin has been tested only with the simple I18n backend that ships
20
+ with Rails.
21
+ I18n texts are read from and written to YAML files under config/locales.
22
+
23
+ This gem is a fork of the original https://github.com/mynewsdesk/translate
24
+ and also includes work from this fork: https://github.com/milann/translate
25
+ EOF
26
+ gem.email = 'romanbsd@yahoo.com'
27
+ gem.homepage = 'https://github.com/romanbsd/translate'
28
+ gem.authors = ['Peter Marklund', 'Milan Novota', 'Roman Shterenzon']
29
+ gem.add_dependency 'ya2yaml', '~> 0.30' # For UTF-8 support in YAML
30
+ end
31
+ Jeweler::GemcutterTasks.new
32
+ rescue LoadError
33
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,165 @@
1
+ class TranslateController < ActionController::Base
2
+ # It seems users with active_record_store may get a "no :secret given" error if we don't disable csrf protection,
3
+ skip_before_filter :verify_authenticity_token
4
+
5
+ prepend_view_path(File.join(File.dirname(__FILE__), "..", "views"))
6
+ layout 'translate'
7
+
8
+ before_filter :init_translations
9
+ before_filter :set_locale
10
+
11
+ def index
12
+ initialize_keys
13
+ filter_by_key_pattern
14
+ filter_by_text_pattern
15
+ filter_by_translated_or_changed
16
+ sort_keys
17
+ paginate_keys
18
+ @total_entries = @keys.size
19
+ end
20
+
21
+ def translate
22
+ I18n.backend.store_translations(@to_locale, Translate::Keys.to_deep_hash(params[:key]))
23
+ Translate::Storage.new(@to_locale).write_to_file
24
+ Translate::Log.new(@from_locale, @to_locale, params[:key].keys).write_to_file
25
+ force_init_translations # Force reload from YAML file
26
+ flash[:notice] = "Translations stored"
27
+ redirect_to params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern).merge({:action => :index})
28
+ end
29
+
30
+ def reload
31
+ Translate::Keys.files = nil
32
+ redirect_to :action => 'index'
33
+ end
34
+
35
+ private
36
+ def initialize_keys
37
+ @files = Translate::Keys.files
38
+ @keys = (@files.keys.map(&:to_s) + Translate::Keys.new.i18n_keys(@from_locale)).uniq
39
+ @keys.reject! do |key|
40
+ from_text = lookup(@from_locale, key)
41
+ # When translating from one language to another, make sure there is a text to translate from.
42
+ # Always exclude non string translation objects as we don't support editing them in the UI.
43
+ (@from_locale != @to_locale && !from_text.present?) || (from_text.present? && !from_text.is_a?(String))
44
+ end
45
+ end
46
+
47
+ def lookup(locale, key)
48
+ I18n.backend.send(:lookup, locale, key)
49
+ end
50
+ helper_method :lookup
51
+
52
+ def filter_by_translated_or_changed
53
+ params[:filter] ||= 'all'
54
+ return if params[:filter] == 'all'
55
+ @keys.reject! do |key|
56
+ case params[:filter]
57
+ when 'untranslated'
58
+ lookup(@to_locale, key).present?
59
+ when 'translated'
60
+ lookup(@to_locale, key).blank?
61
+ when 'changed'
62
+ old_from_text(key).blank? || lookup(@from_locale, key) == old_from_text(key)
63
+ else
64
+ raise "Unknown filter '#{params[:filter]}'"
65
+ end
66
+ end
67
+ end
68
+
69
+ def filter_by_key_pattern
70
+ return if params[:key_pattern].blank?
71
+ @keys.reject! do |key|
72
+ case params[:key_type]
73
+ when "starts_with"
74
+ !key.starts_with?(params[:key_pattern])
75
+ when "contains"
76
+ key.index(params[:key_pattern]).nil?
77
+ else
78
+ raise "Unknown key_type '#{params[:key_type]}'"
79
+ end
80
+ end
81
+ end
82
+
83
+ def filter_by_text_pattern
84
+ return if params[:text_pattern].blank?
85
+ @keys.reject! do |key|
86
+ case params[:text_type]
87
+ when 'contains'
88
+ !lookup(@from_locale, key).present? || !lookup(@from_locale, key).to_s.downcase.index(params[:text_pattern].downcase)
89
+ when 'equals'
90
+ !lookup(@from_locale, key).present? || lookup(@from_locale, key).to_s.downcase != params[:text_pattern].downcase
91
+ else
92
+ raise "Unknown text_type '#{params[:text_type]}'"
93
+ end
94
+ end
95
+ end
96
+
97
+ def sort_keys
98
+ params[:sort_by] ||= "key"
99
+ case params[:sort_by]
100
+ when "key"
101
+ @keys.sort!
102
+ when "text"
103
+ @keys.sort! do |key1, key2|
104
+ if lookup(@from_locale, key1).present? && lookup(@from_locale, key2).present?
105
+ lookup(@from_locale, key1).to_s.downcase <=> lookup(@from_locale, key2).to_s.downcase
106
+ elsif lookup(@from_locale, key1).present?
107
+ -1
108
+ else
109
+ 1
110
+ end
111
+ end
112
+ else
113
+ raise "Unknown sort_by '#{params[:sort_by]}'"
114
+ end
115
+ end
116
+
117
+ def paginate_keys
118
+ params[:page] ||= 1
119
+ @paginated_keys = @keys[offset, per_page]
120
+ end
121
+
122
+ def offset
123
+ (params[:page].to_i - 1) * per_page
124
+ end
125
+
126
+ def per_page
127
+ 50
128
+ end
129
+ helper_method :per_page
130
+
131
+ def init_translations
132
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
133
+ end
134
+
135
+ def force_init_translations
136
+ I18n.backend.send(:init_translations)
137
+ end
138
+
139
+ def default_locale
140
+ I18n.default_locale
141
+ end
142
+
143
+ def set_locale
144
+ session[:from_locale] ||= default_locale
145
+ session[:to_locale] ||= :en
146
+ session[:from_locale] = params[:from_locale] if params[:from_locale].present?
147
+ session[:to_locale] = params[:to_locale] if params[:to_locale].present?
148
+ @from_locale = session[:from_locale].to_sym
149
+ @to_locale = session[:to_locale].to_sym
150
+ end
151
+
152
+ def old_from_text(key)
153
+ return @old_from_text[key] if @old_from_text && @old_from_text[key]
154
+ @old_from_text = {}
155
+ text = key.split(".").inject(log_hash) do |hash, k|
156
+ hash ? hash[k] : nil
157
+ end
158
+ @old_from_text[key] = text
159
+ end
160
+ helper_method :old_from_text
161
+
162
+ def log_hash
163
+ @log_hash ||= Translate::Log.new(@from_locale, @to_locale, {}).read
164
+ end
165
+ end
@@ -0,0 +1,45 @@
1
+ module TranslateHelper
2
+ def simple_filter(labels, param_name = 'filter', selected_value = nil)
3
+ selected_value ||= params[param_name]
4
+ filter = []
5
+ labels.each do |item|
6
+ if item.is_a?(Array)
7
+ type, label = item
8
+ else
9
+ type = label = item
10
+ end
11
+ if type.to_s == selected_value.to_s
12
+ filter << "<i>#{label}</i>"
13
+ else
14
+ link_params = params.merge({param_name.to_s => type})
15
+ link_params.merge!({"page" => nil}) if param_name.to_s != "page"
16
+ filter << link_to(label, link_params)
17
+ end
18
+ end
19
+ filter.join(" | ")
20
+ end
21
+
22
+ def n_lines(text, line_size)
23
+ n_lines = 1
24
+ if text.present?
25
+ n_lines = text.split("\n").size
26
+ if n_lines == 1 && text.length > line_size
27
+ n_lines = text.length / line_size + 1
28
+ end
29
+ end
30
+ n_lines
31
+ end
32
+
33
+ def translate_javascript_includes
34
+ sources = []
35
+ if File.exists?(File.join(Rails.root, "public", "javascripts", "prototype.js"))
36
+ sources << "/javascripts/prototype.js"
37
+ else
38
+ sources << "http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
39
+ end
40
+ sources << "http://www.google.com/jsapi"
41
+ sources.map do |src|
42
+ %Q{<script src="#{src}" type="text/javascript"></script>}
43
+ end.join("\n")
44
+ end
45
+ end
@@ -0,0 +1,359 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
6
+ <title><%= h(@page_title) %></title>
7
+
8
+ <%= raw translate_javascript_includes %>
9
+ <script type="text/javascript">
10
+ google.load("language", "1");
11
+
12
+ function getGoogleTranslation(id, text, from_language, to_language) {
13
+ text = text.replace(/\{\{/, '__').replace(/\}\}/, '__')
14
+ google.language.translate(text, from_language, to_language, function(result) {
15
+ if (!result.error) {
16
+ result_text = result.translation.unescapeHTML().gsub(/__(.+)__/, function(match){
17
+ return '{{' + match[1] + '}}';
18
+ });
19
+ Form.Element.setValue(id, result_text);
20
+ }
21
+ });
22
+
23
+ }
24
+
25
+ /*
26
+ prototypeUtils.js from http://jehiah.com/
27
+ Licensed under Creative Commons.
28
+ version 1.0 December 20 2005
29
+
30
+ Contains:
31
+ + Form.Element.setValue()
32
+ + unpackToForm()
33
+
34
+ */
35
+
36
+ /* Form.Element.setValue("fieldname/id","valueToSet") */
37
+ Form.Element.setValue = function(element,newValue) {
38
+ element_id = element;
39
+ element = $(element);
40
+ if (!element){element = document.getElementsByName(element_id)[0];}
41
+ if (!element){return false;}
42
+ var method = element.tagName.toLowerCase();
43
+ var parameter = Form.Element.SetSerializers[method](element,newValue);
44
+ }
45
+
46
+ Form.Element.SetSerializers = {
47
+ input: function(element,newValue) {
48
+ switch (element.type.toLowerCase()) {
49
+ case 'submit':
50
+ case 'hidden':
51
+ case 'password':
52
+ case 'text':
53
+ return Form.Element.SetSerializers.textarea(element,newValue);
54
+ case 'checkbox':
55
+ case 'radio':
56
+ return Form.Element.SetSerializers.inputSelector(element,newValue);
57
+ }
58
+ return false;
59
+ },
60
+
61
+ inputSelector: function(element,newValue) {
62
+ fields = document.getElementsByName(element.name);
63
+ for (var i=0;i<fields.length;i++){
64
+ if (fields[i].value == newValue){
65
+ fields[i].checked = true;
66
+ }
67
+ }
68
+ },
69
+
70
+ textarea: function(element,newValue) {
71
+ element.value = newValue;
72
+ },
73
+
74
+ select: function(element,newValue) {
75
+ var value = '', opt, index = element.selectedIndex;
76
+ for (var i=0;i< element.options.length;i++){
77
+ if (element.options[i].value == newValue){
78
+ element.selectedIndex = i;
79
+ return true;
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ function unpackToForm(data){
86
+ for (i in data){
87
+ Form.Element.setValue(i,data[i].toString());
88
+ }
89
+ }
90
+
91
+ </script>
92
+
93
+
94
+ <style type="text/css">
95
+ /*reset.css*/
96
+ /* v1.0 | 20080212 */
97
+ html, body, div, span, applet, object, iframe,
98
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
99
+ a, abbr, acronym, address, big, cite, code,
100
+ del, dfn, em, font, img, ins, kbd, q, s, samp,
101
+ small, strike, strong, sub, sup, tt, var,
102
+ b, u, i, center,
103
+ dl, dt, dd, ol, ul, li,
104
+ fieldset, form, label, legend,
105
+ table, caption, tbody, tfoot, thead, tr, th, td {
106
+ margin: 0;
107
+ padding: 0;
108
+ border: 0;
109
+ outline: 0;
110
+ font-size: 100%;
111
+ vertical-align: baseline;
112
+ background: transparent;
113
+ }
114
+ body {
115
+ line-height: 1;
116
+ }
117
+ ol, ul {
118
+ list-style: none;
119
+ }
120
+ blockquote, q {
121
+ quotes: none;
122
+ }
123
+ blockquote:before, blockquote:after,
124
+ q:before, q:after {
125
+ content: '';
126
+ content: none;
127
+ }
128
+
129
+ /* remember to define focus styles! */
130
+ :focus {
131
+ outline: 0;
132
+ }
133
+
134
+ /* remember to highlight inserts somehow! */
135
+ ins {
136
+ text-decoration: none;
137
+ }
138
+ del {
139
+ text-decoration: line-through;
140
+ }
141
+
142
+ /* tables still need 'cellspacing="0"' in the markup */
143
+ table {
144
+ border-collapse: collapse;
145
+ border-spacing: 0;
146
+ }
147
+ /*clear fix*/
148
+ .clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;}
149
+ .clearfix{display:inline-block;}
150
+ html[xmlns] .clearfix {
151
+ display: block;
152
+ }
153
+ * html .clearfix{height:1%;}
154
+ /*start layout*/
155
+ body{
156
+ background:#fff;
157
+ color:#333;
158
+ font-size:75%;
159
+ font-family:Arial;
160
+ margin:2em auto;
161
+ line-height:1.5em;
162
+ }
163
+ textarea,input,select{
164
+ font-family:Arial;
165
+ font-size:1em;
166
+ }
167
+ h1{
168
+ color:#d46021;
169
+ font-size:2em;
170
+ margin-bottom:0.5em;
171
+ }
172
+ h2{
173
+ text-align:left;
174
+ color:#d46021;
175
+ font-size:1.3em;
176
+ padding-left:0;
177
+ }
178
+ a{
179
+ color:#2158C7;
180
+ }
181
+ div#container{
182
+ width:960px;
183
+ margin:0 auto;
184
+ font-size:1em;
185
+ }
186
+ /*paging*/
187
+ div.paging{
188
+ margin-bottom:1em;
189
+ text-align:left;
190
+ }
191
+ div.paging div{
192
+ border:solid 1px red;
193
+ margin:1em 1em 0;
194
+ padding:0.5em;
195
+ border:solid 1px #d5d6d5;
196
+ background:#f1f1f1;
197
+ }
198
+ ul.paging{
199
+ display:inline-block;
200
+ }
201
+ ul.paging li{
202
+ display:block;
203
+ margin:0.2em 0;
204
+ float:left;
205
+ }
206
+ ul.paging li.selected a{
207
+ color:#fff;
208
+ background:#2158C7;
209
+ font-weight:bold;
210
+ padding:0.5em 0.7em;
211
+ }
212
+ ul.paging li a{
213
+ display:inline-block;
214
+ line-height:1em;
215
+ padding:0.5em 0.5em;
216
+ }
217
+ /*forms filter*/
218
+ fieldset{
219
+ padding:1em;
220
+ margin:1em;
221
+ border:solid 2px #d46021;
222
+ }
223
+ legend{
224
+ font-size:1.2em;
225
+ font-weight:bold;
226
+ padding:0 1em;
227
+ padding-bottom:0.5em;
228
+ }
229
+ label{
230
+ font-weight:bold;
231
+ }
232
+ fieldset span{padding-right:0.5em;}
233
+ div#show-sort label,
234
+ div#languages label,
235
+ div#filter-pattern label{
236
+ display:inline-block;
237
+ width:100px;
238
+ line-height:2em;
239
+ }
240
+ div#show-sort select,
241
+ div#languages select,
242
+ div#filter-pattern select{
243
+ width:120px;
244
+ margin-right:0.5em;
245
+ }
246
+ div#show-sort input.text-default,
247
+ div#languages input.text-default,
248
+ div#filter-pattern input.text-default{
249
+ width:200px;
250
+ }
251
+ p.hits{
252
+ margin-top:1em;
253
+ }
254
+ /*translation edit*/
255
+ div.translations{
256
+ margin:1em;
257
+ padding:1em;
258
+ border:solid 2px #d46021;
259
+ }
260
+ div.translations h2{
261
+ margin-bottom:1em;
262
+ }
263
+ p.translate{
264
+ background:red;
265
+ border:solid 1px #d5d6d5;
266
+ background:#f1f1f1;
267
+ margin:0.5em;
268
+ padding:0.7em 0.5em 0.5em 1.5em;
269
+ }
270
+ div.translation{
271
+ padding:1em;
272
+ border-bottom:solid 0.2em #d46021;
273
+ margin:0 1em 1em 1.6em;
274
+ }
275
+ div.translation input, div.translation textarea{
276
+ width:98%;
277
+ margin:1em 0;
278
+ display:inline-block;
279
+ padding:0.3em;
280
+ }
281
+ div.translation textarea{
282
+ height:50px;
283
+ }
284
+ div.translation em strong{
285
+ color:#333;
286
+ padding-right:0.5em;
287
+ }
288
+ p.translation em{
289
+ display:block;
290
+ font-size:0.8333em;
291
+ }
292
+ div.translation a{
293
+ padding:1em;
294
+ }
295
+ div.translation input.btnDefault{
296
+ margin:0 0 1em;
297
+ width:auto;
298
+ }
299
+ .focus-text{
300
+ font-weight:bold;
301
+ }
302
+ div.selected{
303
+ margin:0 1em 1em 1em;
304
+ border-left:solid 0.6em #d46021;
305
+ border-right:solid 0.2em #d46021;
306
+ border-top:solid 0.2em #d46021;
307
+ background:#f1f1f1;
308
+ }
309
+ .display{display:block !important;}
310
+ /*feedback*/
311
+ div#notice, div#error {
312
+ font-size:1em;
313
+ margin:1em;
314
+ padding: 1em;
315
+ border: 1px solid red;
316
+ }
317
+ div#notice span, div#error span{
318
+ font-size:1.5em;
319
+ }
320
+
321
+ div#error {
322
+ background-color: #F3C6CC;
323
+ color: red;
324
+ }
325
+ div#notice {
326
+ border-color: #72A974;
327
+ color: #597B5C;
328
+ background-color: #BCFFBD;
329
+ }
330
+ </style>
331
+ <script type="text/javascript">
332
+ onload = function (){
333
+ $$("div.translation input, div.translation textarea").each(function (e){
334
+ Event.observe(e,'focus', function (elm){
335
+ this.up(1).down(".translation-text").addClassName("focus-text");
336
+ this.up(1).addClassName("selected");
337
+ });
338
+ Event.observe(e,'blur', function (elm,e){
339
+ this.up(1).down(".translation-text").removeClassName("focus-text");
340
+ this.up(1).removeClassName("selected");
341
+ });
342
+ });
343
+ }
344
+ </script>
345
+ </head>
346
+ <body>
347
+ <div id="container">
348
+ <% if @page_title -%><h1><%=h @page_title %></h1><% end -%>
349
+ <% [:notice, :error].each do |message| %>
350
+ <%if flash[message] %>
351
+ <div id="<%= message %>">
352
+ <span><%= h(flash[message]) if flash[message] %></span>
353
+ </div>
354
+ <% end %>
355
+ <% end %>
356
+ <%= yield %>
357
+ </div>
358
+ </body>
359
+ </html>