i18n-instrument 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +141 -0
- data/i18n-instrument.gemspec +22 -0
- data/lib/assets/javascripts/i18n/instrument.js.erb +33 -0
- data/lib/i18n/instrument.rb +20 -0
- data/lib/i18n/instrument/configurator.rb +43 -0
- data/lib/i18n/instrument/middleware.rb +122 -0
- data/lib/i18n/instrument/railtie.rb +10 -0
- data/lib/i18n/instrument/version.rb +5 -0
- data/spec/Rakefile +8 -0
- data/spec/app/controllers/tests_controller.rb +6 -0
- data/spec/config/application.rb +12 -0
- data/spec/config/initializers/i18n_instrument.rb +1 -0
- data/spec/config/initializers/secret_token.rb +1 -0
- data/spec/config/routes.rb +4 -0
- data/spec/javascript_middleware_spec.rb +59 -0
- data/spec/log/development.log +0 -0
- data/spec/log/test.log +5362 -0
- data/spec/ruby_middleware_spec.rb +102 -0
- data/spec/spec_helper.rb +21 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a0463e97ab45fd96ee54d900280a3afa10ee6d95
|
4
|
+
data.tar.gz: 83c48ca90a3e6a82cc40c9a8cf33af38c59a4e20
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 310fca52c96c4119221e0f285a02a81291324aad34daa0e14ceba50c933446699deb04a0ea97849d57a53245a04afca0aa2fb6b1e5ceb351af172b1bab6b1c47
|
7
|
+
data.tar.gz: bf0c3eb4be1e07e60e1aef5aaf45fc7b76c789ce844fa0ca4e3a29ac30c103e4bac23fc2e5621bda42c4d9df58008cc08347b50e6b2b6362ba4e293ff4c85dde
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Lumos Labs, Inc.
|
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.md
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# i18n-instrument
|
2
|
+
Instrument calls to I18n.t in Ruby and JavaScript in your Rails app.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
Add it to your Gemfile:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'i18n-instrument', require: 'i18n/instrument'
|
10
|
+
```
|
11
|
+
|
12
|
+
## The Problem
|
13
|
+
|
14
|
+
Platforms like iOS and Android make it easy to tell which of your localization strings are currently in use - just do a bit of grepping or run a script and voilà! Once you've identified them, you can remove any unused, crufty strings from your translation files and move on with your life.
|
15
|
+
|
16
|
+
Due to Ruby's dynamic nature and the fact that you can never tell what Rails is actually doing, identifying unused strings is much more difficult. The static analysis that worked so well with iOS and Android projects won't work for your Rails app.
|
17
|
+
|
18
|
+
Consider this ERB template. It lives in app/views/products/index.html.erb:
|
19
|
+
|
20
|
+
```HTML+ERB
|
21
|
+
<div class="description">
|
22
|
+
<%= I18n.t('.description') %>: <%= @product.description %>
|
23
|
+
</div>
|
24
|
+
```
|
25
|
+
|
26
|
+
Under the hood, the i18n gem (which provides Rails' localization implementation) fully qualifies `.description` into `products.index.description`. Because this qualification is done at runtime, static analysis (i.e. grepping) won't be able to identify all the strings your app is currently using.
|
27
|
+
|
28
|
+
The problem is compounded by the fact that the key you pass to `I18n.t` is just a string, and can therefore be generated in any way the Ruby language allows, for example:
|
29
|
+
|
30
|
+
```HTML+ERB
|
31
|
+
<div class="fields">
|
32
|
+
<% @product.attributes.each_pair do |attr, value|
|
33
|
+
<div class="<%= attr %>">
|
34
|
+
<%= I18n.t(".#{attr}") %>: <%= value %>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
37
|
+
</div>
|
38
|
+
```
|
39
|
+
First of all, I sincerely hope you never write code like this - it's probably bad practice to loop over *all* the attributes in your model and print them out (security blah blah blah). Hopefully my example illustrates the problem however - namely that the `I18n.t` method can accept any string, including string variables, interpolated strings, and the rest. Unless your static analyzer is very clever, it won't be able to tell you which of your localization strings are currently being used.
|
40
|
+
|
41
|
+
## Ok, so what should I do about it?
|
42
|
+
|
43
|
+
So glad you asked.
|
44
|
+
|
45
|
+
The only foolproof way to collect information about the localization strings your app is using is during runtime. This gem, i18n-instrument, is capable of annotating calls to `I18n.t` in Ruby/Rails and Javascript (provided you're using the capable [i18n-js gem](https://github.com/fnando/i18n-js)). Whenever one of these methods is called, i18n-instrument will fire the `on_record` callback and pass you some useful information. From there, the possibilities are endless. You could log the information to the console, write it down somewhere useful, save it to a database, you name it.
|
46
|
+
|
47
|
+
## Callbacks
|
48
|
+
|
49
|
+
* **`on_lookup(key : String, value : String)`**: Fired whenever `I18n.t` is called. Yields the localization key (i.e. `es.products.index.description`) and the corresponding translation string.
|
50
|
+
|
51
|
+
* **`on_record(params : Hash)`**: Similar to `on_lookup` but comes with a bunch of useful information.
|
52
|
+
|
53
|
+
The `params` hash contains the following keys (all values are strings):
|
54
|
+
|
55
|
+
* **`controller`**: the controller that served the request.
|
56
|
+
* **`action`**: the action that served the request.
|
57
|
+
* **`trace`**: the filename and line number from the first application stack frame, i.e. the place in your code `I18n.t` was called.
|
58
|
+
* **`key`**: the localization key.
|
59
|
+
* **`source`**: either "ruby" or "javascript".
|
60
|
+
* **`locale`**: the value of `I18n.locale` in Ruby or Javascript.
|
61
|
+
|
62
|
+
* **`on_error(e : Exception)`**: Fired whenever an error occurs. The default behavior is to re-raise the exception. You may want to add your own exception handling callback so your app doesn't crash. See the configuration section below for details.
|
63
|
+
|
64
|
+
* **`on_check_enabled() : Boolean`**: Fired on every lookup. The return value must be `true` if the lookup should be recorded and `false` otherwise. The default behavior is to look for the existence of a file named config/enable\_i18n\_instrumentation. If the file exists, `I18n.t` calls are recorded. If not, calls are not recorded.
|
65
|
+
|
66
|
+
## Configuration
|
67
|
+
|
68
|
+
i18n-instrument is a piece of Rack middleware. It sits in between the Internet and your Rails app. You can configure it any time before your app is finished booting. I'd suggest doing it in a Rails initializer, for example config/initializers/i18n\_instrument.rb:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
I18n::Instrument.configure do |config|
|
72
|
+
# The first stack trace line that begins with this string will be passed to
|
73
|
+
# `on_record` as the `trace` value. Defaults to `Rails.root`.
|
74
|
+
config.stack_trace_prefix = 'custom/path/to/app'
|
75
|
+
|
76
|
+
# The URL you want your app to send js instrumentation requests to. Defaults
|
77
|
+
# to '/i18n/instrument.json'.
|
78
|
+
config.js_endpoint = '/my_i18n/foo.json'
|
79
|
+
|
80
|
+
# all of the callback methods are available here
|
81
|
+
|
82
|
+
config.on_record do |params|
|
83
|
+
# print params to the rails log
|
84
|
+
Rails.logger.info(params.inspect)
|
85
|
+
end
|
86
|
+
|
87
|
+
config.on_lookup do |key, value|
|
88
|
+
puts "Looked up i18n key #{key} and got value #{value}"
|
89
|
+
end
|
90
|
+
|
91
|
+
config.on_error do |e|
|
92
|
+
# report errors to our error aggregation service
|
93
|
+
Rollbar.error(e)
|
94
|
+
end
|
95
|
+
|
96
|
+
config.on_check_enabled do
|
97
|
+
# always enable
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
Be careful not to call `I18n::Instrument.configure` more than once - doing so will replace any existing configuration you may have already done. Instead, try this:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# replace any existing on_record behavior, but leave all other configuration intact
|
106
|
+
I18n::Instrument.config.on_record do |params|
|
107
|
+
...
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Javascript Land
|
112
|
+
|
113
|
+
Using i18n-instrument in Javascript is pretty straightforward - just include it in your application.js file:
|
114
|
+
|
115
|
+
```javascript
|
116
|
+
//= require i18n
|
117
|
+
//= require i18n/instrument
|
118
|
+
```
|
119
|
+
Make sure to include i18n/instrument ***after*** i18n. This is critical since i18n-instrument overrides i18n's `t` method.
|
120
|
+
|
121
|
+
Also, don't forget to enable Javascript instrumentation by adding this line, probably at the bottom of application.js:
|
122
|
+
|
123
|
+
```javascript
|
124
|
+
I18n.instrumentation_enabled = true
|
125
|
+
```
|
126
|
+
|
127
|
+
## Requirements
|
128
|
+
|
129
|
+
i18n-instrument is designed to work with Rails 4 and up, although it has not been tested under Rails 5 yet.
|
130
|
+
|
131
|
+
## Testing
|
132
|
+
|
133
|
+
Run the test suite with `bundle exec rspec`.
|
134
|
+
|
135
|
+
## Authors
|
136
|
+
|
137
|
+
* Cameron C. Dutro ([@camertron](https://github.com/camertron)) on behalf of Lumos Labs, Inc.
|
138
|
+
|
139
|
+
## License
|
140
|
+
|
141
|
+
Licensed under the MIT license.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), 'lib')
|
2
|
+
require 'i18n/instrument/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'i18n-instrument'
|
6
|
+
s.version = ::I18n::Instrument::VERSION
|
7
|
+
s.authors = ['Cameron Dutro']
|
8
|
+
s.email = ['camertron@gmail.com']
|
9
|
+
s.homepage = 'https://github.com/lumoslabs/i18n-instrument'
|
10
|
+
|
11
|
+
s.description = s.summary = 'Instrument calls to I18n.t in Ruby and JavaScript.'
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
s.has_rdoc = true
|
15
|
+
|
16
|
+
s.add_dependency 'i18n-debug', '~> 1.0'
|
17
|
+
s.add_dependency 'railties', '~> 4'
|
18
|
+
s.add_dependency 'request_store', '~> 1.0'
|
19
|
+
|
20
|
+
s.require_path = 'lib'
|
21
|
+
s.files = Dir['{lib,spec}/**/*', 'README.md', 'i18n-instrument.gemspec', 'LICENSE']
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
(function() {
|
2
|
+
if (I18n.translate == null) {
|
3
|
+
return;
|
4
|
+
}
|
5
|
+
|
6
|
+
var originalFunction = I18n.translate
|
7
|
+
|
8
|
+
// override original t and translate functions
|
9
|
+
I18n.t = I18n.translate = function(scope, options) {
|
10
|
+
if (I18n.instrumentation_enabled) {
|
11
|
+
// get fully qualified key
|
12
|
+
var opts = I18n.prepareOptions(options);
|
13
|
+
var key = I18n.getFullScope(scope, opts);
|
14
|
+
var data = {
|
15
|
+
key: key,
|
16
|
+
locale: I18n.locale,
|
17
|
+
url: window.location.href
|
18
|
+
};
|
19
|
+
|
20
|
+
// make request to instrumentation middleware
|
21
|
+
$.ajax({
|
22
|
+
url: '<%= I18n::Instrument.config.js_endpoint %>',
|
23
|
+
type: 'post',
|
24
|
+
data: JSON.stringify(data),
|
25
|
+
dataType: 'json',
|
26
|
+
headers: {'Content-Type': 'application/json'}
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
// use `call` to be able to pass in `I18n` as `this`
|
31
|
+
return originalFunction.call(I18n, scope, options);
|
32
|
+
}
|
33
|
+
})();
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'i18n/instrument/railtie'
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Instrument
|
5
|
+
autoload :Configurator, 'i18n/instrument/configurator'
|
6
|
+
autoload :Middleware, 'i18n/instrument/middleware'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
def configure
|
12
|
+
@config = Configurator.new
|
13
|
+
yield @config if block_given?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# set default config
|
19
|
+
Instrument.configure
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module I18n
|
2
|
+
module Instrument
|
3
|
+
class Configurator
|
4
|
+
DEFAULT_ENABLED_FILE = File.join('config', 'enable_i18n_instrumentation')
|
5
|
+
DEFAULT_JS_ENDPOINT = '/i18n/instrument.json'
|
6
|
+
|
7
|
+
attr_accessor :js_endpoint, :stack_trace_prefix
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@js_endpoint = DEFAULT_JS_ENDPOINT
|
11
|
+
@stack_trace_prefix = Rails.root.to_s
|
12
|
+
|
13
|
+
@on_check_enabled_proc = -> do
|
14
|
+
Rails.root.join(DEFAULT_ENABLED_FILE).exist?
|
15
|
+
end
|
16
|
+
|
17
|
+
@on_error_proc = ->(e) { raise e }
|
18
|
+
@on_record_proc = ->(*) { }
|
19
|
+
@on_lookup_proc = ->(*) { }
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_lookup(&block)
|
23
|
+
block ? @on_lookup_proc = block : @on_lookup_proc
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_record(&block)
|
27
|
+
block ? @on_record_proc = block : @on_record_proc
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_error(&block)
|
31
|
+
block ? @on_error_proc = block : @on_error_proc
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_check_enabled(&block)
|
35
|
+
block ? @on_check_enabled_proc = block : @on_check_enabled_proc
|
36
|
+
end
|
37
|
+
|
38
|
+
def enabled?
|
39
|
+
@on_check_enabled_proc.call
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'i18n/debug'
|
2
|
+
require 'request_store'
|
3
|
+
|
4
|
+
module I18n
|
5
|
+
module Instrument
|
6
|
+
class Middleware
|
7
|
+
# used to store request information in the request store
|
8
|
+
STORE_KEY = :i18n_instrumentation
|
9
|
+
|
10
|
+
# canned response sent to js instrumentation requests
|
11
|
+
JS_RESPONSE = [
|
12
|
+
200, {
|
13
|
+
'Content-Type' => 'application/json',
|
14
|
+
'Content-Length' => 2
|
15
|
+
}, [
|
16
|
+
'{}'
|
17
|
+
]
|
18
|
+
]
|
19
|
+
|
20
|
+
def initialize(app)
|
21
|
+
@app = app
|
22
|
+
|
23
|
+
# this will fire on every call to I18n.t
|
24
|
+
I18n::Debug.on_lookup do |key, value|
|
25
|
+
next unless enabled?
|
26
|
+
config.on_lookup.call(key, value)
|
27
|
+
|
28
|
+
begin
|
29
|
+
# find the first application-specific line in the stack trace
|
30
|
+
raw_trace = filter_stack_trace(::Kernel.caller)
|
31
|
+
trace = raw_trace.split(":in `").first if raw_trace
|
32
|
+
|
33
|
+
# grab path params (set in `call` method below)
|
34
|
+
url = store.fetch(STORE_KEY, {}).fetch(:url, nil)
|
35
|
+
|
36
|
+
if url.present? && trace.present?
|
37
|
+
record_translation_lookup(
|
38
|
+
url: url, trace: trace, key: key,
|
39
|
+
locale: I18n.locale.to_s, source: 'ruby'
|
40
|
+
)
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
config.on_error.call(e)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
if i18n_js_request?(env)
|
50
|
+
handle_i18n_js_request(env)
|
51
|
+
else
|
52
|
+
handle_regular_request(env)
|
53
|
+
end
|
54
|
+
rescue => e
|
55
|
+
config.on_error.call(e)
|
56
|
+
@app.call(env)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def filter_stack_trace(trace)
|
62
|
+
trace.find { |line| line.start_with?(config.stack_trace_prefix) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def record_translation_lookup(url:, trace:, key:, locale:, source:)
|
66
|
+
config.on_record.call({
|
67
|
+
url: url, trace: trace, key: key,
|
68
|
+
source: source, locale: locale
|
69
|
+
})
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_regular_request(env)
|
73
|
+
return @app.call(env) unless enabled?
|
74
|
+
|
75
|
+
# store params from env in request storage so they can be used in the
|
76
|
+
# I18n on_lookup callback above
|
77
|
+
store[STORE_KEY] = { url: env['REQUEST_URI'] }
|
78
|
+
|
79
|
+
@app.call(env)
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_i18n_js_request(env)
|
83
|
+
return JS_RESPONSE unless enabled?
|
84
|
+
|
85
|
+
body = JSON.parse(env['rack.input'].read)
|
86
|
+
url = body['url']
|
87
|
+
key = body['key']
|
88
|
+
locale = body['locale']
|
89
|
+
|
90
|
+
if url.present? && key.present?
|
91
|
+
record_translation_lookup(
|
92
|
+
url: url, trace: nil, key: key,
|
93
|
+
locale: locale, source: 'javascript'
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
JS_RESPONSE
|
98
|
+
end
|
99
|
+
|
100
|
+
def i18n_js_request?(env)
|
101
|
+
env.fetch('REQUEST_URI', '').include?(js_endpoint) &&
|
102
|
+
env.fetch('REQUEST_METHOD', '').upcase == 'POST'
|
103
|
+
end
|
104
|
+
|
105
|
+
def config
|
106
|
+
I18n::Instrument.config
|
107
|
+
end
|
108
|
+
|
109
|
+
def js_endpoint
|
110
|
+
config.js_endpoint
|
111
|
+
end
|
112
|
+
|
113
|
+
def enabled?
|
114
|
+
config.enabled?
|
115
|
+
end
|
116
|
+
|
117
|
+
def store
|
118
|
+
RequestStore.store
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module I18n
|
2
|
+
module Instrument
|
3
|
+
class Railtie < Rails::Railtie
|
4
|
+
initializer 'i18n.instrument.middleware' do |app|
|
5
|
+
app.config.assets.paths << File.expand_path('../../../assets/javascripts', __FILE__)
|
6
|
+
app.middleware.use(I18n::Instrument::Middleware)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/spec/Rakefile
ADDED