i18n-tasks 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +8 -3
- data/Gemfile +2 -0
- data/README.md +12 -12
- data/lib/i18n/tasks.rb +2 -0
- data/lib/i18n/tasks/base_task.rb +17 -148
- data/lib/i18n/tasks/data/yaml.rb +4 -4
- data/lib/i18n/tasks/fuzzy_source_keys.rb +20 -0
- data/lib/i18n/tasks/ignore_keys.rb +24 -0
- data/lib/i18n/tasks/key_pattern_matching.rb +39 -0
- data/lib/i18n/tasks/output/terminal.rb +16 -3
- data/lib/i18n/tasks/plural_keys.rb +17 -0
- data/lib/i18n/tasks/prefill.rb +7 -0
- data/lib/i18n/tasks/relative_keys.rb +13 -0
- data/lib/i18n/tasks/translation_data.rb +58 -0
- data/lib/i18n/tasks/usage_search.rb +60 -0
- data/lib/i18n/tasks/version.rb +1 -1
- data/spec/fixtures/config/i18n-tasks.yml +2 -2
- data/spec/readme_spec.rb +9 -0
- data/spec/support/i18n_tasks_output_matcher.rb +4 -5
- metadata +11 -3
- data/lib/i18n/tasks/task_helpers.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5394593809b1be9eabef55d84ee0cf8ce692884
|
4
|
+
data.tar.gz: 5629b833e0ef043538a387a3eff45a1e16fdbc6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80141dbe9b294f9e83b3d1a812aa5f3381e2909ddf8175cec2df66dae8da310e6aedda8d36bb7a11c511d04e41a59972469eabe6475b88f0cfa0065034efa9b7
|
7
|
+
data.tar.gz: a0c620f7af53c0aa9f820afc057eb658f8c84d43caad4a9affdb1854d1b5ecacf76ceef4288b7265eaecca2382e0f3a8e6de226e7c21205c9aaf0d3a54a441a1
|
data/CHANGES.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
## v0.1.6
|
2
|
+
|
3
|
+
* New key pattern syntax for i18n-tasks.yml a la globbing (@glebm)
|
4
|
+
|
1
5
|
## v0.1.5
|
2
|
-
|
6
|
+
|
7
|
+
* Removed get_locale_data, added data configuration options (@glebm)
|
3
8
|
|
4
9
|
## v0.1.4
|
5
10
|
|
@@ -8,10 +13,10 @@
|
|
8
13
|
|
9
14
|
## v0.1.3
|
10
15
|
|
11
|
-
* detect countable keys as used for unused task
|
16
|
+
* detect countable keys as used for unused task (@glebm)
|
12
17
|
* account for non-string keys coming from yaml (thanks @lichtamberg)
|
13
18
|
|
14
19
|
## v0.1.2
|
15
20
|
|
16
21
|
* added grep config options (thanks @dmke)
|
17
|
-
* improved terminal output
|
22
|
+
* improved terminal output (@glebm)
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -42,7 +42,7 @@ Tasks may incorrectly report framework i18n keys as missing. You can add `config
|
|
42
42
|
# do not report these keys as missing (both on blank value and no key)
|
43
43
|
ignore_missing:
|
44
44
|
- devise.errors.unauthorized # ignore this key
|
45
|
-
- pagination.views
|
45
|
+
- pagination.views.* # ignore the whole pattern
|
46
46
|
|
47
47
|
# do not report these keys when they have the same value as the base locale version
|
48
48
|
ignore_eq_base:
|
@@ -53,11 +53,19 @@ ignore_eq_base:
|
|
53
53
|
|
54
54
|
# do not report these keys as unused
|
55
55
|
ignore_unused:
|
56
|
-
- category
|
56
|
+
- category.*
|
57
57
|
|
58
58
|
# do not report these keys ever
|
59
59
|
ignore:
|
60
|
-
- kaminari
|
60
|
+
- kaminari.*
|
61
|
+
|
62
|
+
# where to read locale data from
|
63
|
+
data:
|
64
|
+
# read paths for a given %{locale} (supports globs)
|
65
|
+
paths:
|
66
|
+
- 'config/locales/%{locale}.yml'
|
67
|
+
# you can also implement a custom storage layer, see the yaml one below
|
68
|
+
class: I18n::Tasks::Data::Yaml
|
61
69
|
|
62
70
|
# search configuration (grep arguments)
|
63
71
|
grep:
|
@@ -70,15 +78,6 @@ grep:
|
|
70
78
|
- '*.html*'
|
71
79
|
# explicitly exclude files (default: blank = exclude no files)
|
72
80
|
exclude: '*.js'
|
73
|
-
|
74
|
-
# where to read locale data from
|
75
|
-
data:
|
76
|
-
# file patterns for a given %{locale} (supports globs)
|
77
|
-
paths:
|
78
|
-
# default:
|
79
|
-
- 'config/locales/%{locale}.yml'
|
80
|
-
# you can also implement a custom loader, use the default as an example
|
81
|
-
class: I18n::Tasks::Data::Yaml
|
82
81
|
```
|
83
82
|
|
84
83
|
## HTML report
|
@@ -89,6 +88,7 @@ While i18n-tasks does not provide an HTML version of the report, it's easy to ro
|
|
89
88
|
|
90
89
|
This was originally developed for [Zuigo](http://zuigo.com/), a platform to organize and discover events.
|
91
90
|
|
91
|
+
[MIT license](/LICENSE.txt)
|
92
92
|
|
93
93
|
|
94
94
|
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/glebm/i18n-tasks/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
|
data/lib/i18n/tasks.rb
CHANGED
data/lib/i18n/tasks/base_task.rb
CHANGED
@@ -1,158 +1,27 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
require 'term/ansicolor'
|
3
|
-
require 'i18n/tasks/
|
3
|
+
require 'i18n/tasks/usage_search'
|
4
|
+
require 'i18n/tasks/fuzzy_source_keys'
|
5
|
+
require 'i18n/tasks/plural_keys'
|
6
|
+
require 'i18n/tasks/relative_keys'
|
7
|
+
require 'i18n/tasks/translation_data'
|
8
|
+
require 'i18n/tasks/ignore_keys'
|
4
9
|
|
5
10
|
module I18n
|
6
11
|
module Tasks
|
7
12
|
class BaseTask
|
8
|
-
include
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def
|
18
|
-
|
19
|
-
conf = config[:data] || {}
|
20
|
-
@source = if conf[:class]
|
21
|
-
conf[:class].constantize.new(conf.except(:class))
|
22
|
-
else
|
23
|
-
I18n::Tasks::Data::Yaml.new(
|
24
|
-
paths: Array(conf[:paths].presence || ['config/locales/%{locale}.yml'])
|
25
|
-
)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# main locale file path (for writing to)
|
30
|
-
# @return [String]
|
31
|
-
def locale_file_path(locale)
|
32
|
-
"config/locales/#{locale}.yml"
|
33
|
-
end
|
34
|
-
|
35
|
-
# find all keys in the source (relative keys are returned in absolutized)
|
36
|
-
# @return [Array<String>]
|
37
|
-
def find_source_keys
|
38
|
-
@source_keys ||= begin
|
39
|
-
if (grep_out = run_grep)
|
40
|
-
grep_out.split("\n").map { |r|
|
41
|
-
key = r.match(/['"](.*?)['"]/)[1]
|
42
|
-
if key.start_with? '.'
|
43
|
-
absolutize_key key, r.split(':')[0]
|
44
|
-
else
|
45
|
-
key
|
46
|
-
end
|
47
|
-
}.uniq.reject { |k| k !~ /^[\w.\#{}]+$/ }
|
48
|
-
else
|
49
|
-
[]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# whether the key is used in the source
|
55
|
-
def used_key?(key)
|
56
|
-
@used_keys ||= find_source_keys.to_set
|
57
|
-
@used_keys.include?(key)
|
58
|
-
end
|
59
|
-
|
60
|
-
# whether to ignore the key. ignore_type one of :missing, :eq_base, :blank, :unused.
|
61
|
-
# will apply global ignore rules as well
|
62
|
-
def ignore_key?(key, ignore_type, locale = nil)
|
63
|
-
key =~ ignore_pattern(ignore_type, locale)
|
64
|
-
end
|
65
|
-
|
66
|
-
# dynamically generated keys in the source, e.g t("category.#{category_key}")
|
67
|
-
def pattern_key?(key)
|
68
|
-
@pattern_keys_re ||= compile_start_with_re(pattern_key_prefixes)
|
69
|
-
key =~ @pattern_keys_re
|
70
|
-
end
|
71
|
-
|
72
|
-
# keys in the source that end with a ., e.g. t("category.#{cat.i18n_key}") or t("category." + category.key)
|
73
|
-
def pattern_key_prefixes
|
74
|
-
@pattern_keys_prefixes ||=
|
75
|
-
find_source_keys.select { |k| k =~ /\#{.*?}/ || k.ends_with?('.') }.map { |k| k.split(/\.?#/)[0].presence }.compact
|
76
|
-
end
|
77
|
-
|
78
|
-
# whether the value for key exists in locale (defaults: base_locale)
|
79
|
-
def key_has_value?(key, locale = base_locale)
|
80
|
-
t(locale_data(locale)[locale], key).present?
|
13
|
+
include UsageSearch
|
14
|
+
include PluralKeys
|
15
|
+
include RelativeKeys
|
16
|
+
include FuzzySourceKeys
|
17
|
+
include TranslationData
|
18
|
+
include IgnoreKeys
|
19
|
+
|
20
|
+
# i18n-tasks config (defaults + config/i18n-tasks.yml)
|
21
|
+
# @return [Hash{String => String,Hash,Array}]
|
22
|
+
def config
|
23
|
+
I18n::Tasks.config
|
81
24
|
end
|
82
|
-
|
83
|
-
# traverse hash, yielding with full key and value
|
84
|
-
# @param hash [Hash{String => String,Hash}] translation data to traverse
|
85
|
-
# @yield [full_key, value] yields full key and value for every translation in #hash
|
86
|
-
# @return [nil]
|
87
|
-
def traverse(path = '', hash)
|
88
|
-
q = [[path, hash]]
|
89
|
-
until q.empty?
|
90
|
-
path, value = q.pop
|
91
|
-
if value.is_a?(Hash)
|
92
|
-
value.each { |k, v| q << ["#{path}.#{k}", v] }
|
93
|
-
else
|
94
|
-
yield path[1..-1], value
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# translation of the key found in the passed hash or nil
|
100
|
-
# @return [String,nil]
|
101
|
-
def t(hash, key)
|
102
|
-
key.split('.').inject(hash) { |r, seg| r[seg] if r }
|
103
|
-
end
|
104
|
-
|
105
|
-
# @param key [String] relative i18n key (starts with a .)
|
106
|
-
# @param path [String] path to the file containing the key
|
107
|
-
# @return [String] absolute version of the key
|
108
|
-
def absolutize_key(key, path)
|
109
|
-
# normalized path
|
110
|
-
path = Pathname.new(File.expand_path path).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
111
|
-
# key prefix based on path
|
112
|
-
prefix = path.gsub(%r(app/views/|(\.[^/]+)*$), '').tr('/', '.').gsub(%r(\._), '.')
|
113
|
-
"#{prefix}#{key}"
|
114
|
-
end
|
115
|
-
|
116
|
-
PLURAL_KEY_RE = /\.(?:zero|one|two|few|many|other)$/
|
117
|
-
|
118
|
-
# @param key [String] i18n key
|
119
|
-
# @param data [Hash{String => String,Hash}] locale data
|
120
|
-
# @return the base form if the key is a specific plural form (e.g. apple for apple.many), and the key as passed otherwise
|
121
|
-
def depluralize_key(key, data)
|
122
|
-
return key if key !~ PLURAL_KEY_RE || t(data, key).is_a?(Hash)
|
123
|
-
parent_key = key.split('.')[0..-2] * '.'
|
124
|
-
plural_versions = t(data, parent_key)
|
125
|
-
if plural_versions.is_a?(Hash) && plural_versions.all? { |k, v| ".#{k}" =~ PLURAL_KEY_RE && !v.is_a?(Hash) }
|
126
|
-
parent_key
|
127
|
-
else
|
128
|
-
key
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
|
133
|
-
# @return [String] default i18n locale
|
134
|
-
def base_locale
|
135
|
-
I18n.default_locale.to_s
|
136
|
-
end
|
137
|
-
|
138
|
-
# @return [Hash{String => String,Hash}] default i18n locale data
|
139
|
-
def base_locale_data
|
140
|
-
locale_data(base_locale)[base_locale]
|
141
|
-
end
|
142
|
-
|
143
|
-
# Run grep searching for source keys and return grep output
|
144
|
-
# @return [String] output of the grep command
|
145
|
-
def run_grep
|
146
|
-
args = ['grep', '-HoRI']
|
147
|
-
[:include, :exclude].each do |opt|
|
148
|
-
next unless (val = grep_config[opt]).present?
|
149
|
-
args += Array(val).map { |v| "--#{opt}=#{v}" }
|
150
|
-
end
|
151
|
-
args += [%q{\\bt(\\?\\s*['"]\\([^'"]*\\)['"]}, *grep_config[:paths]]
|
152
|
-
args.compact!
|
153
|
-
run_command *args
|
154
|
-
end
|
155
|
-
|
156
25
|
end
|
157
26
|
end
|
158
27
|
end
|
data/lib/i18n/tasks/data/yaml.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
module I18n::Tasks
|
2
2
|
module Data
|
3
3
|
class Yaml
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :options
|
5
5
|
|
6
6
|
def initialize(options)
|
7
|
-
@
|
7
|
+
@options = options
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(locale)
|
11
|
-
paths.map do |path|
|
11
|
+
options[:paths].map do |path|
|
12
12
|
Dir.glob path % { locale: locale }
|
13
13
|
end.flatten.map do |locale_file|
|
14
14
|
YAML.load_file locale_file
|
15
15
|
end.inject({}) do |hash, locale_data|
|
16
|
-
hash.deep_merge! locale_data
|
16
|
+
hash.deep_merge! locale_data || {}
|
17
17
|
hash
|
18
18
|
end
|
19
19
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'i18n/tasks/key_pattern_matching'
|
2
|
+
|
3
|
+
# e.g t("category.#{category_key}")
|
4
|
+
module I18n::Tasks
|
5
|
+
module FuzzySourceKeys
|
6
|
+
include KeyPatternMatching
|
7
|
+
|
8
|
+
# dynamically generated keys in the source, e.g t("category.#{category_key}")
|
9
|
+
def pattern_key?(key)
|
10
|
+
@pattern_keys_re ||= compile_start_with_re(pattern_key_prefixes)
|
11
|
+
!!(key =~ @pattern_keys_re)
|
12
|
+
end
|
13
|
+
|
14
|
+
# keys in the source that end with a ., e.g. t("category.#{cat.i18n_key}") or t("category." + category.key)
|
15
|
+
def pattern_key_prefixes
|
16
|
+
@pattern_keys_prefixes ||=
|
17
|
+
find_source_keys.select { |k| k =~ /\#{.*?}/ || k.ends_with?('.') }.map { |k| k.split(/\.?#/)[0].presence }.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module I18n::Tasks::IgnoreKeys
|
2
|
+
# whether to ignore the key. ignore_type one of :missing, :eq_base, :blank, :unused.
|
3
|
+
# will apply global ignore rules as well
|
4
|
+
def ignore_key?(key, ignore_type, locale = nil)
|
5
|
+
key =~ ignore_pattern(ignore_type, locale)
|
6
|
+
end
|
7
|
+
|
8
|
+
# @param type [:missing, :eq_base, :unused] type
|
9
|
+
# @param locale [String] only when type is :eq_base
|
10
|
+
# @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
|
11
|
+
def ignore_pattern(type, locale = nil)
|
12
|
+
((@ignore_patterns ||= HashWithIndifferentAccess.new)[type] ||= {})[locale] = begin
|
13
|
+
global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
|
14
|
+
if type_ignore.is_a?(Array)
|
15
|
+
patterns = global + type_ignore
|
16
|
+
elsif type_ignore.is_a?(Hash)
|
17
|
+
# ignore per locale
|
18
|
+
patterns = global + (type_ignore[:all] || []) +
|
19
|
+
type_ignore.select { |k, v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
|
20
|
+
end
|
21
|
+
compile_patterns_re patterns
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module I18n::Tasks::KeyPatternMatching
|
2
|
+
MATCH_NOTHING = /\z\A/
|
3
|
+
|
4
|
+
# one regex to match any
|
5
|
+
def compile_patterns_re(key_patterns)
|
6
|
+
if key_patterns.blank?
|
7
|
+
# match nothing
|
8
|
+
MATCH_NOTHING
|
9
|
+
else
|
10
|
+
/(?:#{ key_patterns.map { |p| key_pattern_to_re p } * '|' })/m
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# convert key.* to key\..*
|
15
|
+
def key_pattern_to_re(key_pattern)
|
16
|
+
if key_pattern.end_with? '.'
|
17
|
+
$stderr.puts %Q(i18n-tasks: Deprecated "#{key_pattern}", please change to "#{key_pattern += '*'}".)
|
18
|
+
end
|
19
|
+
/^#{key_pattern.
|
20
|
+
gsub(/\./, '\.').
|
21
|
+
gsub(/\*/, '.*')}$/
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Array<String>] keys sans passed patterns
|
25
|
+
def exclude_patterns(keys, patterns)
|
26
|
+
pattern_re = compile_patterns_re patterns.select { |p| p.end_with?('.') }
|
27
|
+
(keys - patterns).reject { |k| k =~ pattern_re }
|
28
|
+
end
|
29
|
+
|
30
|
+
# compile prefix matching Regexp from the list of prefixes
|
31
|
+
# @return [Regexp] regexp matching any of the prefixes
|
32
|
+
def compile_start_with_re(prefixes)
|
33
|
+
if prefixes.blank?
|
34
|
+
MATCH_NOTHING # match nothing
|
35
|
+
else
|
36
|
+
/^(?:#{prefixes.map { |p| Regexp.escape(p) }.join('|')})/
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -18,8 +18,12 @@ module I18n
|
|
18
18
|
|
19
19
|
def unused(unused)
|
20
20
|
$stderr.puts bold cyan("Unused i18n keys (#{unused.length})")
|
21
|
-
|
22
|
-
|
21
|
+
if unused.present?
|
22
|
+
key_col_width = unused.max_by { |x| x[0].length }[0].length + 2
|
23
|
+
unused.each { |(key, value)| puts "#{magenta key.ljust(key_col_width)}#{cyan value.to_s.strip}" }
|
24
|
+
else
|
25
|
+
$stderr.puts(bold green 'Good job! Every translation is used!')
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
private
|
@@ -34,7 +38,10 @@ module I18n
|
|
34
38
|
def print_missing_translation(m, opts)
|
35
39
|
locale, key, base_value, status_text = m[:locale], m[:key], m[:base_value].to_s.try(:strip), " #{STATUS_TEXTS[m[:type]]}"
|
36
40
|
|
37
|
-
|
41
|
+
long = base_value.length > 50
|
42
|
+
|
43
|
+
key = magenta "#{key}#{':' if long}".ljust(opts[:key_col_width])
|
44
|
+
base_value = "\n#{indent(base_value, 13)}\n" if long
|
38
45
|
s = if m[:type] == :none
|
39
46
|
"#{red bold locale.ljust(4)} #{status_text} #{key}"
|
40
47
|
else
|
@@ -42,6 +49,12 @@ module I18n
|
|
42
49
|
end
|
43
50
|
puts s
|
44
51
|
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def indent(txt, n = 2)
|
55
|
+
spaces = ' ' * n
|
56
|
+
txt.gsub /^/, spaces
|
57
|
+
end
|
45
58
|
end
|
46
59
|
end
|
47
60
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module I18n::Tasks::PluralKeys
|
2
|
+
PLURAL_KEY_RE = /\.(?:zero|one|two|few|many|other)$/
|
3
|
+
|
4
|
+
# @param key [String] i18n key
|
5
|
+
# @param data [Hash{String => String,Hash}] locale data
|
6
|
+
# @return the base form if the key is a specific plural form (e.g. apple for apple.many), and the key as passed otherwise
|
7
|
+
def depluralize_key(key, data)
|
8
|
+
return key if key !~ PLURAL_KEY_RE || t(data, key).is_a?(Hash)
|
9
|
+
parent_key = key.split('.')[0..-2] * '.'
|
10
|
+
plural_versions = t(data, parent_key)
|
11
|
+
if plural_versions.is_a?(Hash) && plural_versions.all? { |k, v| ".#{k}" =~ PLURAL_KEY_RE && !v.is_a?(Hash) }
|
12
|
+
parent_key
|
13
|
+
else
|
14
|
+
key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/i18n/tasks/prefill.rb
CHANGED
@@ -3,6 +3,7 @@ require 'i18n/tasks/base_task'
|
|
3
3
|
module I18n
|
4
4
|
module Tasks
|
5
5
|
class Prefill < BaseTask
|
6
|
+
# todo refactor to allow configuring output targets
|
6
7
|
def perform
|
7
8
|
# Will also rewrite en, good for ordering
|
8
9
|
I18n.available_locales.map(&:to_s).each do |target_locale|
|
@@ -11,6 +12,12 @@ module I18n
|
|
11
12
|
File.open(locale_file_path(target_locale), 'w'){ |f| f.write prefilled.to_yaml }
|
12
13
|
end
|
13
14
|
end
|
15
|
+
|
16
|
+
# main locale file path (for writing to)
|
17
|
+
# @return [String]
|
18
|
+
def locale_file_path(locale)
|
19
|
+
"config/locales/#{locale}.yml"
|
20
|
+
end
|
14
21
|
end
|
15
22
|
end
|
16
23
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module I18n::Tasks::RelativeKeys
|
2
|
+
|
3
|
+
# @param key [String] relative i18n key (starts with a .)
|
4
|
+
# @param path [String] path to the file containing the key
|
5
|
+
# @return [String] absolute version of the key
|
6
|
+
def absolutize_key(key, path)
|
7
|
+
# normalized path
|
8
|
+
path = Pathname.new(File.expand_path path).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
9
|
+
# key prefix based on path
|
10
|
+
prefix = path.gsub(%r(app/views/|(\.[^/]+)*$), '').tr('/', '.').gsub(%r(\._), '.')
|
11
|
+
"#{prefix}#{key}"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module I18n::Tasks::TranslationData
|
2
|
+
# locale data hash, with locale name as root
|
3
|
+
# @return [Hash{String => String,Hash}] locale data in nested hash format
|
4
|
+
def locale_data(locale)
|
5
|
+
locale = locale.to_s
|
6
|
+
(@locale_data ||= {})[locale] ||= data_source.get(locale) || {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# I18n data provider
|
10
|
+
def data_source
|
11
|
+
return @source if @source
|
12
|
+
conf = config[:data] || {}
|
13
|
+
@source = if conf[:class]
|
14
|
+
conf[:class].constantize.new(conf.except(:class))
|
15
|
+
else
|
16
|
+
I18n::Tasks::Data::Yaml.new(
|
17
|
+
paths: Array(conf[:paths].presence || ['config/locales/%{locale}.yml'])
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# translation of the key found in the passed hash or nil
|
23
|
+
# @return [String,nil]
|
24
|
+
def t(hash, key)
|
25
|
+
key.split('.').inject(hash) { |r, seg| r[seg] if r }
|
26
|
+
end
|
27
|
+
|
28
|
+
# traverse hash, yielding with full key and value
|
29
|
+
# @param hash [Hash{String => String,Hash}] translation data to traverse
|
30
|
+
# @yield [full_key, value] yields full key and value for every translation in #hash
|
31
|
+
# @return [nil]
|
32
|
+
def traverse(path = '', hash)
|
33
|
+
q = [[path, hash]]
|
34
|
+
until q.empty?
|
35
|
+
path, value = q.pop
|
36
|
+
if value.is_a?(Hash)
|
37
|
+
value.each { |k, v| q << ["#{path}.#{k}", v] }
|
38
|
+
else
|
39
|
+
yield path[1..-1], value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String] default i18n locale
|
45
|
+
def base_locale
|
46
|
+
I18n.default_locale.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Hash{String => String,Hash}] default i18n locale data
|
50
|
+
def base_locale_data
|
51
|
+
locale_data(base_locale)[base_locale]
|
52
|
+
end
|
53
|
+
|
54
|
+
# whether the value for key exists in locale (defaults: base_locale)
|
55
|
+
def key_has_value?(key, locale = base_locale)
|
56
|
+
t(locale_data(locale)[locale], key).present?
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'open3'
|
2
|
+
module I18n::Tasks::UsageSearch
|
3
|
+
# grep config, also from config/i18n-tasks.yml
|
4
|
+
# @return [Hash{String => String,Hash,Array}]
|
5
|
+
def grep_config
|
6
|
+
@grep_config ||= (config[:grep] || {}).with_indifferent_access.tap do |conf|
|
7
|
+
conf[:paths] = ['app/'] if conf[:paths].blank?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# whether the key is used in the source
|
12
|
+
def used_key?(key)
|
13
|
+
@used_keys ||= find_source_keys.to_set
|
14
|
+
@used_keys.include?(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
# find all keys in the source (relative keys are returned in absolutized)
|
18
|
+
# @return [Array<String>]
|
19
|
+
def find_source_keys
|
20
|
+
@source_keys ||= begin
|
21
|
+
if (grep_out = run_grep)
|
22
|
+
grep_out.split("\n").map { |r|
|
23
|
+
key = r.match(/['"](.*?)['"]/)[1]
|
24
|
+
if key.start_with? '.'
|
25
|
+
absolutize_key key, r.split(':')[0]
|
26
|
+
else
|
27
|
+
key
|
28
|
+
end
|
29
|
+
}.uniq.reject { |k| k !~ /^[\w.\#{}]+$/ }
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Run grep searching for source keys and return grep output
|
39
|
+
# @return [String] output of the grep command
|
40
|
+
def run_grep
|
41
|
+
args = ['grep', '-HoRI']
|
42
|
+
[:include, :exclude].each do |opt|
|
43
|
+
next unless (val = grep_config[opt]).present?
|
44
|
+
args += Array(val).map { |v| "--#{opt}=#{v}" }
|
45
|
+
end
|
46
|
+
args += [%q{\\bt(\\?\\s*['"]\\([^'"]*\\)['"]}, *grep_config[:paths]]
|
47
|
+
args.compact!
|
48
|
+
run_command *args
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Run command and get only stdout output
|
53
|
+
# @return [String] output
|
54
|
+
# @raise [RuntimeError] if grep returns with exit code other than 0
|
55
|
+
def run_command(*args)
|
56
|
+
o, e, s = Open3.capture3(*args)
|
57
|
+
raise "#{args[0]} failed with status #{s.exitstatus} (stderr: #{e})" unless s.success?
|
58
|
+
o
|
59
|
+
end
|
60
|
+
end
|
data/lib/i18n/tasks/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# do not report these keys as missing:
|
2
2
|
ignore_missing:
|
3
3
|
- ignored_missing_key.a # one key to ignore
|
4
|
-
- ignored_pattern
|
4
|
+
- ignored_pattern.* # ignore the whole pattern
|
5
5
|
|
6
6
|
# do not report these keys when they have the same value as the base locale version
|
7
7
|
ignore_eq_base:
|
@@ -34,8 +34,8 @@ grep:
|
|
34
34
|
|
35
35
|
# how to get the locale data
|
36
36
|
data:
|
37
|
+
class: I18n::Tasks::Data::Yaml
|
37
38
|
paths:
|
38
39
|
# files for a given %{locale}
|
39
40
|
- 'config/locales/%{locale}.yml'
|
40
41
|
- 'config/locales/*.%{locale}.yml'
|
41
|
-
class: I18n::Tasks::Data::Yaml
|
data/spec/readme_spec.rb
ADDED
@@ -7,14 +7,13 @@ RSpec::Matchers.define :be_i18n_keys do |expected|
|
|
7
7
|
def extract_keys(actual)
|
8
8
|
locales = I18n.available_locales.map(&:to_s)
|
9
9
|
actual.split("\n").map { |x|
|
10
|
-
x.strip!
|
11
10
|
key = x.gsub(/\s+/, ' ').split(' ').reverse.detect { |p| p && p.include?('.') }
|
12
11
|
if x =~ locale_re && locales.include?(x[0..1]) && !(key =~ locale_re && locales.include?(key[0..1]))
|
13
|
-
x.split(' ', 2)[0] + '.' + key
|
14
|
-
else
|
15
|
-
key
|
12
|
+
key = x.split(' ', 2)[0] + '.' + key
|
16
13
|
end
|
17
|
-
|
14
|
+
key = key[0..-2] if key.end_with?(':')
|
15
|
+
key
|
16
|
+
}.compact
|
18
17
|
end
|
19
18
|
|
20
19
|
match do |actual|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: i18n-tasks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- glebm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-11-
|
11
|
+
date: 2013-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -128,12 +128,18 @@ files:
|
|
128
128
|
- lib/i18n/tasks.rb
|
129
129
|
- lib/i18n/tasks/base_task.rb
|
130
130
|
- lib/i18n/tasks/data/yaml.rb
|
131
|
+
- lib/i18n/tasks/fuzzy_source_keys.rb
|
132
|
+
- lib/i18n/tasks/ignore_keys.rb
|
133
|
+
- lib/i18n/tasks/key_pattern_matching.rb
|
131
134
|
- lib/i18n/tasks/missing.rb
|
132
135
|
- lib/i18n/tasks/output/terminal.rb
|
136
|
+
- lib/i18n/tasks/plural_keys.rb
|
133
137
|
- lib/i18n/tasks/prefill.rb
|
134
138
|
- lib/i18n/tasks/railtie.rb
|
135
|
-
- lib/i18n/tasks/
|
139
|
+
- lib/i18n/tasks/relative_keys.rb
|
140
|
+
- lib/i18n/tasks/translation_data.rb
|
136
141
|
- lib/i18n/tasks/unused.rb
|
142
|
+
- lib/i18n/tasks/usage_search.rb
|
137
143
|
- lib/i18n/tasks/version.rb
|
138
144
|
- lib/tasks/i18n-tasks.rake
|
139
145
|
- spec/fixtures/app/assets/javascripts/application.js
|
@@ -142,6 +148,7 @@ files:
|
|
142
148
|
- spec/fixtures/app/views/relative/index.html.slim
|
143
149
|
- spec/fixtures/config/i18n-tasks.yml
|
144
150
|
- spec/i18n_tasks_spec.rb
|
151
|
+
- spec/readme_spec.rb
|
145
152
|
- spec/spec_helper.rb
|
146
153
|
- spec/support/fixtures.rb
|
147
154
|
- spec/support/i18n_tasks_output_matcher.rb
|
@@ -178,6 +185,7 @@ test_files:
|
|
178
185
|
- spec/fixtures/app/views/relative/index.html.slim
|
179
186
|
- spec/fixtures/config/i18n-tasks.yml
|
180
187
|
- spec/i18n_tasks_spec.rb
|
188
|
+
- spec/readme_spec.rb
|
181
189
|
- spec/spec_helper.rb
|
182
190
|
- spec/support/fixtures.rb
|
183
191
|
- spec/support/i18n_tasks_output_matcher.rb
|
@@ -1,64 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
require 'open3'
|
3
|
-
|
4
|
-
module I18n
|
5
|
-
module Tasks
|
6
|
-
module TaskHelpers
|
7
|
-
# Run command and get only stdout output
|
8
|
-
# @return [String] output
|
9
|
-
# @raise [RuntimeError] if grep returns with exit code other than 0
|
10
|
-
def run_command(*args)
|
11
|
-
o, e, s = Open3.capture3(*args)
|
12
|
-
raise "#{args[0]} failed with status #{s.exitstatus} (stderr: #{e})" unless s.success?
|
13
|
-
o
|
14
|
-
end
|
15
|
-
|
16
|
-
# compile prefix matching Regexp from the list of prefixes
|
17
|
-
# @return [Regexp] regexp matching any of the prefixes
|
18
|
-
def compile_start_with_re(prefixes)
|
19
|
-
if prefixes.blank?
|
20
|
-
/\Z\A/ # match nothing
|
21
|
-
else
|
22
|
-
/^(?:#{prefixes.map { |p| Regexp.escape(p) }.join('|')})/
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# @return [Array<String>] keys sans passed patterns
|
27
|
-
def exclude_patterns(keys, patterns)
|
28
|
-
pattern_re = compile_start_with_re patterns.select { |p| p.end_with?('.') }
|
29
|
-
(keys - patterns).reject { |k| k =~ pattern_re }
|
30
|
-
end
|
31
|
-
|
32
|
-
# @param type [:missing, :eq_base, :unused] type
|
33
|
-
# @param locale [String] only when type is :eq_base
|
34
|
-
# @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
|
35
|
-
def ignore_pattern(type, locale = nil)
|
36
|
-
((@ignore_patterns ||= HashWithIndifferentAccess.new)[type] ||= {})[locale] = begin
|
37
|
-
global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
|
38
|
-
if type_ignore.is_a?(Array)
|
39
|
-
patterns = global + type_ignore
|
40
|
-
elsif type_ignore.is_a?(Hash)
|
41
|
-
# ignore per locale
|
42
|
-
patterns = global + (type_ignore[:all] || []) +
|
43
|
-
type_ignore.select { |k, v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
|
44
|
-
end
|
45
|
-
compile_start_with_re patterns
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# i18n-tasks config (defaults + config/i18n-tasks.yml)
|
50
|
-
# @return [Hash{String => String,Hash,Array}]
|
51
|
-
def config
|
52
|
-
I18n::Tasks.config
|
53
|
-
end
|
54
|
-
|
55
|
-
# grep config, also from config/i18n-tasks.yml
|
56
|
-
# @return [Hash{String => String,Hash,Array}]
|
57
|
-
def grep_config
|
58
|
-
@grep_config ||= (config[:grep] || {}).with_indifferent_access.tap do |conf|
|
59
|
-
conf[:paths] = ['app/'] if conf[:paths].blank?
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|