i18n-tasks 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](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
|