Skip to content

Commit 5a04033

Browse files
committed
i18nliner(.rb)
this swaps out (most of) our ruby haax with i18nliner test plan: 1. verify string extraction: 1. `rake i18n:generate` before and after this commit 2. confirm `config/locales/generated/en.yml` is identical 2. verify english defaults: 1. use canvas in english 2. confirm everything looks correct 3. verify translation keys/scopes: 1. run canvas w/ RAILS_LOAD_ALL_LOCALES=true and optimized js 2. use canvas in spanish 3. confirm that todo está bien 4. confirm you can now use i18nliner-y features: 1. call `t` without a key 2. use the fancy erb block syntax Change-Id: I979479c0889fe7e31ee0c962a4bd1998ab54d711 Reviewed-on: https://gerrit.instructure.com/42785 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Jennifer Stern <jstern@instructure.com> Product-Review: Jennifer Stern <jstern@instructure.com> QA-Review: Matt Fairbourn <mfairbourn@instructure.com>
1 parent 18b6179 commit 5a04033

24 files changed

Lines changed: 360 additions & 698 deletions

File tree

.i18nignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/db
2+
/guard
3+
/node_modules
4+
/spec
5+
/tmp
6+
/vendor/bundle

Gemfile.d/other_stuff.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
gem 'diff-lcs', '1.2.5', :require => 'diff/lcs'
3939

4040
gem 'ffi', '1.1.5'
41-
gem 'hairtrigger', '0.2.9'
41+
gem 'hairtrigger', '0.2.12'
4242
gem 'ruby2ruby', '2.0.8'
4343
gem 'ruby_parser', '3.6.1'
4444
gem 'hashery', '1.3.0', :require => 'hashery/dictionary'
@@ -47,6 +47,7 @@
4747
gem 'i18n', '0.6.9'
4848
gem 'i18nema', '0.0.8', :platforms => [:mri_20, :mri_21]
4949
gem 'i18nema19', '0.0.8', :platform => :mri_19
50+
gem 'i18nliner', '0.0.8'
5051
gem 'icalendar', '1.5.4'
5152
gem 'ims-lti', '2.0.0.beta.10'
5253
gem 'jammit', :github => 'documentcloud/jammit', :ref => '98b50a67029c2860717485a72a2ff0ae8ec37840'

app/helpers/stream_items_helper.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ def extract_summary(category, item)
138138
when "Assignment"
139139
asset.subject
140140
when "AssessmentRequest"
141-
asset.asset.assignment.title + I18n.t('for'," for ") + asset.asset.user.name
141+
# TODO I18N should use placeholders, not concatenation
142+
asset.asset.assignment.title + " " + I18n.t('for', "for") + " " + asset.asset.user.name
142143
else
143144
nil
144145
end

app/models/content_migration.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def add_warning(user_message, opts={})
239239

240240
def add_import_warning(item_type, item_name, warning)
241241
item_name = CanvasTextHelper.truncate_text(item_name || "", :max_length => 150)
242-
add_warning(t('errors.import_error', "Import Error: ") + "#{item_type} - \"#{item_name}\"", warning)
242+
add_warning(t('errors.import_error', "Import Error:") + " #{item_type} - \"#{item_name}\"", warning)
243243
end
244244

245245
def fail_with_error!(exception_or_info)
@@ -609,7 +609,7 @@ def add_warnings_for_missing_content_links
609609
if item[:missing_links].any?
610610
add_warning(t(:missing_content_links_title, "Missing links found in imported content") + " - #{item[:class]} #{item[:field]}",
611611
{:error_message => "#{item[:class]} #{item[:field]} - " + t(:missing_content_links_message,
612-
"The following references could not be resolved: ") + " " + item[:missing_links].join(', '),
612+
"The following references could not be resolved:") + " " + item[:missing_links].join(', '),
613613
:fix_issue_html_url => item[:url]})
614614
end
615615
end

app/models/incoming_mail/message_handler.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ def bounce_message_strings(subject, error)
9595
case error
9696
when IncomingMail::Errors::ReplyToLockedTopic
9797
ndr_subject = I18n.t('lib.incoming_message_processor.locked_topic.subject', "Message Reply Failed: %{subject}", :subject => subject)
98-
ndr_body = I18n.t('lib.incoming_message_processor.locked_topic.body', <<-BODY, :subject => subject).strip_heredoc
98+
ndr_body = I18n.t('lib.incoming_message_processor.locked_topic.body', <<-BODY, :subject => subject).gsub(/^ +/, '')
9999
The message titled "%{subject}" could not be delivered because the discussion topic is locked. If you are trying to contact someone through Canvas you can try logging in to your account and sending them a message using the Inbox tool.
100100
101101
Thank you,
102102
Canvas Support
103103
BODY
104104
else # including IncomingMessageProcessor::UnknownAddressError
105105
ndr_subject = I18n.t('lib.incoming_message_processor.failure_message.subject', "Message Reply Failed: %{subject}", :subject => subject)
106-
ndr_body = I18n.t('lib.incoming_message_processor.failure_message.body', <<-BODY, :subject => subject).strip_heredoc
106+
ndr_body = I18n.t('lib.incoming_message_processor.failure_message.body', <<-BODY, :subject => subject).gsub(/^ +/, '')
107107
The message titled "%{subject}" could not be delivered. The message was sent to an unknown mailbox address. If you are trying to contact someone through Canvas you can try logging in to your account and sending them a message using the Inbox tool.
108108
109109
Thank you,

app/models/notification.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,7 @@ def category_description
464464
Assignment submission/resubmission
465465
EOS
466466
when 'Submission Comment'
467-
mt(:submission_comment_description, <<-EOS)
468-
Assignment submission comment
469-
470-
EOS
467+
t(:submission_comment_description, "Assignment submission comment")
471468
when 'Grading Policies'
472469
t(:grading_policies_description, 'Course grading policy change')
473470
when 'Invitation'

app/views/shared/_mute_dialog.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div id="mute_dialog" style="display: none; width: 400px;" data-title="<%= t("gradebook.mute_assignment", "Mute Assignment") %>">
22
<%=
3-
mt(:mute_dialog, %{
3+
mt(:mute_dialog, <<-STR)
44
Are you sure you want to mute this assignment? While this assignment
55
is muted, students will not receive new notifications about or be
66
able to see:
@@ -14,7 +14,7 @@ Students will be able to see that this assignment is muted.
1414
1515
Once you have muted this assignment, you can begin sending
1616
notifications again by clicking the "Unmute Assignment" link.
17-
})
17+
STR
1818
%>
1919
</div>
2020

config/initializers/i18n.rb

Lines changed: 54 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
I18n.enforce_available_locales = true
1616

17+
I18nliner.infer_interpolation_values = false
18+
1719
if ENV['LOLCALIZE']
1820
require 'i18n_tasks'
1921
I18n.send :extend, I18nTasks::Lolcalize
@@ -85,116 +87,53 @@ def blabel(method, text = nil, options = {})
8587
end
8688
end
8789

88-
I18n.class_eval do
89-
class << self
90-
attr_writer :localizer
91-
92-
include ::ActionView::Helpers::TextHelper
93-
# if one of the interpolated values is a SafeBuffer (e.g. the result of a
94-
# link_to call) or the string itself is, we don't want anything to get
95-
# double-escaped when output in the view (since string is not html_safe).
96-
# so we escape anything that's not safe prior to interpolation, and make
97-
# sure we return a SafeBuffer.
98-
def interpolate_hash_with_html_safety_awareness(string, values)
99-
if string.html_safe? || values.values.any?{ |v| v.is_a?(ActiveSupport::SafeBuffer) }
100-
values.each_pair{ |key, value| values[key] = ERB::Util.h(value) unless value.html_safe? }
101-
string = ERB::Util.h(string) unless string.html_safe?
102-
end
103-
if string.is_a?(ActiveSupport::SafeBuffer) && string.html_safe?
104-
string.class.new(interpolate_hash_without_html_safety_awareness(string.to_str, values))
105-
else
106-
interpolate_hash_without_html_safety_awareness(string, values)
107-
end
108-
end
109-
110-
alias_method_chain :interpolate_hash, :html_safety_awareness
111-
112-
# removes left padding from %e / %k / %l
113-
def localize_with_whitespace_removal(object, options = {})
114-
localize_without_whitespace_removal(object, options).gsub(/\s{2,}/, ' ').strip
115-
end
116-
alias_method_chain :localize, :whitespace_removal
117-
118-
# Public: If a localizer has been set, use it to set the locale and then
119-
# delete it.
120-
#
121-
# Returns nothing.
122-
def set_locale_with_localizer
123-
if @localizer
124-
self.locale = @localizer.call
125-
@localizer = nil
126-
end
127-
end
128-
129-
def translate_with_default_and_count_magic(key, *args)
130-
set_locale_with_localizer
131-
132-
default = args.shift if args.first.is_a?(String) || args.size > 1
133-
options = args.shift || {}
134-
options[:default] ||= if options[:count]
135-
case default
136-
when String
137-
default =~ /\A[\w\-]+\z/ ? pluralize(options[:count], default) : default
138-
when Hash
139-
case options[:count]
140-
when 0
141-
default[:zero]
142-
when 1
143-
default[:one]
144-
end || default[:other]
145-
else
146-
default
147-
end
148-
else
149-
default
150-
end
151-
152-
begin
153-
result = translate_without_default_and_count_magic(key.to_s.sub(/\A#/, ''), options)
154-
rescue I18n::MissingInterpolationArgument
155-
# if we change an en default and its interpolation logic without
156-
# changing its key, we might have broken translations during the
157-
# window where we're waiting for updated translations. broken as in
158-
# crashy, not just missing. if that's the case, just fall back to
159-
# english, rather than asploding
160-
raise if (options[:locale] || I18n.locale) == I18n.default_locale
161-
return translate_with_default_and_count_magic(key, options.merge(locale: I18n.default_locale))
162-
end
163-
164-
# it's assumed that if you're using any wrappers, you're going
165-
# for html output. so the result will be escaped before being
166-
# wrapped, then the output tagged as html safe.
167-
if wrapper = options[:wrapper]
168-
result = I18n.apply_wrappers(result, wrapper)
169-
end
170-
171-
result
90+
I18n.send(:extend, Module.new {
91+
attr_writer :localizer
92+
93+
# Public: If a localizer has been set, use it to set the locale and then
94+
# delete it.
95+
#
96+
# Returns nothing.
97+
def set_locale_with_localizer
98+
if @localizer
99+
self.locale = @localizer.call
100+
@localizer = nil
172101
end
173-
alias_method_chain :translate, :default_and_count_magic
174-
alias :t :translate
175102
end
176103

177-
WRAPPER_REGEXES = {}
178-
179-
def self.apply_wrappers(string, wrappers)
180-
string = ERB::Util.h(string) unless string.html_safe?
181-
wrappers = { '*' => wrappers } unless wrappers.is_a?(Hash)
182-
wrappers.sort_by { |a| -a.first.length }.each do |sym, replace|
183-
regex = (WRAPPER_REGEXES[sym] ||= %r{#{Regexp.escape(sym)}([^#{Regexp.escape(sym)}]*)#{Regexp.escape(sym)}})
184-
string = string.gsub(regex, replace)
104+
def translate(*args)
105+
set_locale_with_localizer
106+
107+
begin
108+
super
109+
rescue I18n::MissingInterpolationArgument
110+
# if we change an en default and its interpolation logic without
111+
# changing its key, we might have broken translations during the
112+
# window where we're waiting for updated translations. broken as in
113+
# crashy, not just missing. if that's the case, just fall back to
114+
# english, rather than asploding
115+
key, options = I18nliner::CallHelpers.infer_arguments(args)
116+
raise if (options[:locale] || locale) == default_locale
117+
super(key, options.merge(locale: default_locale))
185118
end
186-
string.html_safe
187119
end
120+
alias :t :translate
188121

189-
def self.qualified_locale
190-
I18n.backend.direct_lookup(I18n.locale.to_s, "qualified_locale") || "en-US"
122+
def qualified_locale
123+
backend.direct_lookup(locale.to_s, "qualified_locale") || "en-US"
191124
end
192-
end
125+
})
126+
127+
# see also corresponding extractor logic in
128+
# i18n_extraction/i18nliner_extensions
129+
require "i18n_extraction/i18nliner_scope_extensions"
193130

194131
ActionView::Template.class_eval do
195132
def render_with_i18n_scope(view, *args, &block)
196133
old_i18n_scope = view.i18n_scope
197-
view.i18n_scope = @virtual_path.gsub(/\/_?/, '.') if @virtual_path
134+
if @virtual_path
135+
view.i18n_scope = I18nliner::Scope.new(@virtual_path.gsub(/\/_?/, '.'))
136+
end
198137
render_without_i18n_scope(view, *args, &block)
199138
ensure
200139
view.i18n_scope = old_i18n_scope
@@ -204,51 +143,27 @@ def render_with_i18n_scope(view, *args, &block)
204143

205144
ActionView::Base.class_eval do
206145
attr_accessor :i18n_scope
207-
208-
# can accept either translate(key, default: "default text", option: ...) or
209-
# translate(key, "default text", option: ...). when using the former (default
210-
# in the options), it's treated as if prepended with a # anchor.
211-
def translate(key, *rest)
212-
options = rest.extract_options!
213-
default_in_options = options.has_key?(:default)
214-
default_in_args = !rest.empty?
215-
raise ArgumentError, "wrong arity" if rest.size > 1
216-
raise ArgumentError, "didn't provide default in args or options" if !default_in_args && !default_in_options
217-
raise ArgumentError, "can't provide default in both args and options" if default_in_args && default_in_options
218-
default = default_in_options ? options[:default] : rest.first
219-
key = key.to_s
220-
key = "#{i18n_scope}.#{key}" unless default_in_options || key.sub!(/\A#/, '')
221-
I18n.translate(key, default, options)
222-
end
223-
alias :t :translate
224146
end
225147

226148
ActionController::Base.class_eval do
227-
def translate(key, default, options = {})
228-
key = key.to_s
229-
key = "#{controller_name}.#{key}" unless key.sub!(/\A#/, '')
230-
I18n.translate(key, default, options)
149+
def i18n_scope
150+
@i18n_scope ||= I18nliner::Scope.new(controller_path.tr('/', '.'))
231151
end
232-
alias :t :translate
233152
end
234153

235154
ActiveRecord::Base.class_eval do
236155
include I18nUtilities
237156
extend I18nUtilities
238157

239-
def translate(key, default, options = {})
240-
self.class.translate(key, default, options)
158+
def i18n_scope
159+
self.class.i18n_scope
241160
end
242-
alias :t :translate
243161

244-
class << self
245-
def translate(key, default, options = {})
246-
key = key.to_s
247-
key = "#{name.underscore}.#{key}" unless key.sub!(/\A#/, '')
248-
I18n.translate(key, default, options)
249-
end
250-
alias :t :translate
162+
def self.i18n_scope
163+
@i18n_scope ||= I18nliner::Scope.new(self.class.name.underscore)
164+
end
251165

166+
class << self
252167
# so that we don't load up the locales until we need them
253168
LOCALE_LIST = []
254169
def LOCALE_LIST.include?(item)
@@ -273,10 +188,15 @@ def validates_locale(*args)
273188
end
274189

275190
ActionMailer::Base.class_eval do
191+
def i18n_scope
192+
@i18n_scope ||= I18nliner::Scope.new("#{mailer_name}.#{action_name}")
193+
end
194+
276195
def translate(key, default, options = {})
277-
key = key.to_s
278-
key = "#{mailer_name}.#{action_name}.#{key}" unless key.sub!(/\A#/, '')
279-
I18n.translate(key, default, options)
196+
key, options = I18nliner::CallHelpers.infer_arguments(args)
197+
options = inferpolate(options) if I18nliner.infer_interpolation_values
198+
options[:i18n_scope] = i18n_scope
199+
I18n.translate(key, options)
280200
end
281201
alias :t :translate
282202
end

gems/i18n_extraction/i18n_extraction.gemspec

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ Gem::Specification.new do |spec|
1414
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
1515
spec.require_paths = ["lib"]
1616

17-
spec.add_dependency "sexp_processor", "4.2.1"
18-
spec.add_dependency "ruby_parser", "3.6.1"
17+
spec.add_dependency "sexp_processor", "~> 4.2"
18+
spec.add_dependency "ruby_parser", "~> 3.6"
1919
spec.add_dependency "activesupport", ">= 3.2", "< 4.2"
20+
spec.add_dependency "i18nliner", "0.0.8"
2021

2122
spec.add_development_dependency "bundler", "~> 1.5"
2223
spec.add_development_dependency "rake"
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
require "sexp_processor"
2-
require "ruby_parser"
31
require "json"
42
require "active_support/all"
53

4+
require "i18n_extraction/i18nliner_extensions"
5+
66
module I18nExtraction
77
require "i18n_extraction/abstract_extractor"
88
require "i18n_extraction/handlebars_extractor"
99
require "i18n_extraction/js_extractor"
10-
require "i18n_extraction/ruby_extractor"
1110
end

0 commit comments

Comments
 (0)