i18n-processes 0.1.0
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 +7 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +12 -0
- data/bin/i18n-processes +28 -0
- data/bin/i18n-processes.cmd +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/i18n-processes.gemspec +64 -0
- data/lib/i18n/processes/base_process.rb +47 -0
- data/lib/i18n/processes/cli.rb +208 -0
- data/lib/i18n/processes/command/collection.rb +21 -0
- data/lib/i18n/processes/command/commander.rb +43 -0
- data/lib/i18n/processes/command/commands/data.rb +107 -0
- data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
- data/lib/i18n/processes/command/commands/health.rb +26 -0
- data/lib/i18n/processes/command/commands/meta.rb +38 -0
- data/lib/i18n/processes/command/commands/missing.rb +86 -0
- data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
- data/lib/i18n/processes/command/commands/tree.rb +119 -0
- data/lib/i18n/processes/command/commands/usages.rb +69 -0
- data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
- data/lib/i18n/processes/command/dsl.rb +56 -0
- data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
- data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
- data/lib/i18n/processes/command/options/common.rb +41 -0
- data/lib/i18n/processes/command/options/data.rb +95 -0
- data/lib/i18n/processes/command/options/locales.rb +36 -0
- data/lib/i18n/processes/command_error.rb +13 -0
- data/lib/i18n/processes/commands.rb +31 -0
- data/lib/i18n/processes/configuration.rb +132 -0
- data/lib/i18n/processes/console_context.rb +76 -0
- data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
- data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
- data/lib/i18n/processes/data/file_formats.rb +111 -0
- data/lib/i18n/processes/data/file_system.rb +14 -0
- data/lib/i18n/processes/data/file_system_base.rb +205 -0
- data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
- data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
- data/lib/i18n/processes/data/tree/node.rb +204 -0
- data/lib/i18n/processes/data/tree/nodes.rb +97 -0
- data/lib/i18n/processes/data/tree/siblings.rb +333 -0
- data/lib/i18n/processes/data/tree/traversal.rb +190 -0
- data/lib/i18n/processes/data.rb +87 -0
- data/lib/i18n/processes/google_translation.rb +125 -0
- data/lib/i18n/processes/html_keys.rb +16 -0
- data/lib/i18n/processes/ignore_keys.rb +30 -0
- data/lib/i18n/processes/key_pattern_matching.rb +37 -0
- data/lib/i18n/processes/locale_list.rb +19 -0
- data/lib/i18n/processes/locale_pathname.rb +17 -0
- data/lib/i18n/processes/logging.rb +37 -0
- data/lib/i18n/processes/missing_keys.rb +122 -0
- data/lib/i18n/processes/path.rb +42 -0
- data/lib/i18n/processes/plural_keys.rb +41 -0
- data/lib/i18n/processes/rainbow_utils.rb +13 -0
- data/lib/i18n/processes/references.rb +101 -0
- data/lib/i18n/processes/reports/base.rb +71 -0
- data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
- data/lib/i18n/processes/reports/terminal.rb +252 -0
- data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
- data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
- data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
- data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
- data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
- data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
- data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
- data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
- data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
- data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
- data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
- data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
- data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
- data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
- data/lib/i18n/processes/scanners/scanner.rb +17 -0
- data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
- data/lib/i18n/processes/split_key.rb +68 -0
- data/lib/i18n/processes/stats.rb +24 -0
- data/lib/i18n/processes/string_interpolation.rb +16 -0
- data/lib/i18n/processes/unused_keys.rb +23 -0
- data/lib/i18n/processes/used_keys.rb +177 -0
- data/lib/i18n/processes/version.rb +7 -0
- data/lib/i18n/processes.rb +69 -0
- data/source/p1/_messages/zh/article.properties +9 -0
- data/source/p1/_messages/zh/company.properties +62 -0
- data/source/p1/_messages/zh/devices.properties +40 -0
- data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/source/p1/_messages/zh/meetingBooking.properties +18 -0
- data/source/p1/_messages/zh/office-areas.properties +64 -0
- data/source/p1/_messages/zh/orders.properties +25 -0
- data/source/p1/_messages/zh/schedulings.properties +7 -0
- data/source/p1/_messages/zh/tag.properties +2 -0
- data/source/p1/_messages/zh/ticket.properties +9 -0
- data/source/p1/_messages/zh/visitor.properties +5 -0
- data/source/p1/messages +586 -0
- data/source/p2/orders.properties +25 -0
- data/source/p2/schedulings.properties +7 -0
- data/source/p2/tag.properties +2 -0
- data/source/p2/ticket.properties +9 -0
- data/source/p2/visitor.properties +5 -0
- data/source/zh.messages.ts +30 -0
- data/translated/en/p1/_messages/zh/article.properties +9 -0
- data/translated/en/p1/_messages/zh/company.properties +62 -0
- data/translated/en/p1/_messages/zh/devices.properties +40 -0
- data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
- data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
- data/translated/en/p1/_messages/zh/orders.properties +25 -0
- data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
- data/translated/en/p1/_messages/zh/tag.properties +2 -0
- data/translated/en/p1/_messages/zh/ticket.properties +9 -0
- data/translated/en/p1/_messages/zh/visitor.properties +5 -0
- data/translated/en/p1/messages +586 -0
- data/translated/en/p2/orders.properties +25 -0
- data/translated/en/p2/schedulings.properties +7 -0
- data/translated/en/p2/tag.properties +2 -0
- data/translated/en/p2/ticket.properties +9 -0
- data/translated/en/p2/visitor.properties +5 -0
- data/translated/en/zh.messages.ts +30 -0
- data/translation/en/article.properties +9 -0
- data/translation/en/company.properties +56 -0
- data/translation/en/meeting-rooms.properties +87 -0
- data/translation/en/meetingBooking.properties +14 -0
- data/translation/en/messages.en +164 -0
- data/translation/en/office-areas.properties +51 -0
- data/translation/en/orders.properties +26 -0
- data/translation/en/tag.properties +2 -0
- data/translation/en/translated +1263 -0
- data/translation/en/visitor.properties +4 -0
- metadata +408 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'i18n/processes/scanners/results/key_occurrences'
|
|
4
|
+
|
|
5
|
+
module I18n::Processes::Scanners
|
|
6
|
+
# Describes the API of a scanner.
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
# @since 0.9.0
|
|
10
|
+
class Scanner
|
|
11
|
+
# @abstract
|
|
12
|
+
# @return [Array<Results::KeyOccurrences>] the keys found by this scanner and their occurrences.
|
|
13
|
+
def keys
|
|
14
|
+
fail 'Unimplemented'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'i18n/processes/scanners/scanner'
|
|
4
|
+
|
|
5
|
+
module I18n::Processes::Scanners
|
|
6
|
+
# Run multiple {Scanner Scanners} and merge their results.
|
|
7
|
+
# @note The scanners are run concurrently. A thread is spawned per each scanner.
|
|
8
|
+
# @since 0.9.0
|
|
9
|
+
class ScannerMultiplexer < Scanner
|
|
10
|
+
# @param scanners [Array<Scanner>]
|
|
11
|
+
def initialize(scanners:)
|
|
12
|
+
@scanners = scanners
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Collect the results of all the scanners. Occurrences of a key from multiple scanners are merged.
|
|
16
|
+
#
|
|
17
|
+
# @note The scanners are run concurrently. A thread is spawned per each scanner.
|
|
18
|
+
# @return (see Scanner#keys)
|
|
19
|
+
def keys
|
|
20
|
+
Results::KeyOccurrences.merge_keys collect_results.flatten(1)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# @return [Array<Array<Results::KeyOccurrences>>]
|
|
26
|
+
def collect_results
|
|
27
|
+
return [@scanners[0].keys] if @scanners.length == 1
|
|
28
|
+
Array.new(@scanners.length).tap do |results|
|
|
29
|
+
results_mutex = Mutex.new
|
|
30
|
+
@scanners.map.with_index do |scanner, i|
|
|
31
|
+
Thread.start do
|
|
32
|
+
scanner_results = scanner.keys
|
|
33
|
+
results_mutex.synchronize do
|
|
34
|
+
results[i] = scanner_results
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end.each(&:join)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module SplitKey
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# split a key by dots (.)
|
|
8
|
+
# dots inside braces or parenthesis are not split on
|
|
9
|
+
#
|
|
10
|
+
# split_key 'a.b' # => ['a', 'b']
|
|
11
|
+
# split_key 'a.#{b.c}' # => ['a', '#{b.c}']
|
|
12
|
+
# split_key 'a.b.c', 2 # => ['a', 'b.c']
|
|
13
|
+
def split_key(key, max = Float::INFINITY)
|
|
14
|
+
parts = []
|
|
15
|
+
pos = 0
|
|
16
|
+
return [key] if max == 1
|
|
17
|
+
key_parts(key) do |part|
|
|
18
|
+
parts << part
|
|
19
|
+
pos += part.length + 1
|
|
20
|
+
if parts.length + 1 >= max
|
|
21
|
+
parts << key[pos..-1] unless pos == key.length
|
|
22
|
+
break
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
parts
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def last_key_part(key)
|
|
29
|
+
last = nil
|
|
30
|
+
key_parts(key) { |part| last = part }
|
|
31
|
+
last
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# yield each key part
|
|
35
|
+
# dots inside braces or parenthesis are not split on
|
|
36
|
+
def key_parts(key, &block)
|
|
37
|
+
return enum_for(:key_parts, key) unless block
|
|
38
|
+
nesting = PARENS
|
|
39
|
+
counts = PARENS_ZEROS # dup'd later if key contains parenthesis
|
|
40
|
+
delim = '.'
|
|
41
|
+
from = to = 0
|
|
42
|
+
key.each_char do |char|
|
|
43
|
+
if char == delim && PARENS_ZEROS == counts
|
|
44
|
+
block.yield key[from...to]
|
|
45
|
+
from = to = (to + 1)
|
|
46
|
+
else
|
|
47
|
+
nest_i, nest_inc = nesting[char]
|
|
48
|
+
if nest_i
|
|
49
|
+
counts = counts.dup if counts.frozen?
|
|
50
|
+
counts[nest_i] += nest_inc
|
|
51
|
+
end
|
|
52
|
+
to += 1
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
block.yield(key[from...to]) if from < to && to <= key.length
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
PARENS = %w({} [] ()).each_with_object({}) do |s, h|
|
|
60
|
+
i = h.size / 2
|
|
61
|
+
h[s[0].freeze] = [i, 1].freeze
|
|
62
|
+
h[s[1].freeze] = [i, -1].freeze
|
|
63
|
+
end.freeze
|
|
64
|
+
PARENS_ZEROS = Array.new(PARENS.size, 0).freeze
|
|
65
|
+
private_constant :PARENS
|
|
66
|
+
private_constant :PARENS_ZEROS
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module Stats
|
|
5
|
+
def forest_stats(forest)
|
|
6
|
+
key_count = forest.leaves.count
|
|
7
|
+
locale_count = forest.count
|
|
8
|
+
if key_count.zero?
|
|
9
|
+
{ key_count: 0 }
|
|
10
|
+
else
|
|
11
|
+
{
|
|
12
|
+
locales: forest.map(&:key).join(', '),
|
|
13
|
+
key_count: key_count,
|
|
14
|
+
locale_count: locale_count,
|
|
15
|
+
per_locale_avg: forest.inject(0) { |sum, f| sum + f.leaves.count } / locale_count,
|
|
16
|
+
key_segments_avg: format(
|
|
17
|
+
'%.1f', forest.leaves.inject(0) { |sum, node| sum + node.walk_to_root.count - 1 } / key_count.to_f
|
|
18
|
+
),
|
|
19
|
+
value_chars_avg: forest.leaves.inject(0) { |sum, node| sum + node.value.to_s.length } / key_count
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module StringInterpolation
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def interpolate_soft(s, t = {})
|
|
8
|
+
return s unless s
|
|
9
|
+
t.each do |k, v|
|
|
10
|
+
pat = "%{#{k}}"
|
|
11
|
+
s = s.gsub pat, v.to_s if s.include?(pat)
|
|
12
|
+
end
|
|
13
|
+
s
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module I18n::Processes
|
|
6
|
+
module UnusedKeys
|
|
7
|
+
def unused_keys(locales: nil, strict: nil)
|
|
8
|
+
locales = Array(locales).presence || self.locales
|
|
9
|
+
locales.map { |locale| unused_tree(locale: locale, strict: strict) }.compact.reduce(:merge!)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param [String] locale
|
|
13
|
+
# @param [Boolean] strict if true, do not match dynamic keys
|
|
14
|
+
def unused_tree(locale: base_locale, strict: nil)
|
|
15
|
+
used_key_names = used_tree(strict: true).key_names
|
|
16
|
+
collapse_plural_nodes!(data[locale].select_keys do |key, _node|
|
|
17
|
+
!ignore_key?(key, :unused) &&
|
|
18
|
+
(strict || !used_in_expr?(key)) &&
|
|
19
|
+
!used_key_names.include?(depluralize_key(key, locale))
|
|
20
|
+
end)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'find'
|
|
4
|
+
require 'i18n/processes/scanners/pattern_with_scope_scanner'
|
|
5
|
+
require 'i18n/processes/scanners/ruby_ast_scanner'
|
|
6
|
+
require 'i18n/processes/scanners/scanner_multiplexer'
|
|
7
|
+
require 'i18n/processes/scanners/files/caching_file_finder_provider'
|
|
8
|
+
require 'i18n/processes/scanners/files/caching_file_reader'
|
|
9
|
+
|
|
10
|
+
# Require the pattern mapper even though it's not used by i18n-tasks directly.
|
|
11
|
+
# This allows the user to use it in config/i18n-processes.yml without having to require it.
|
|
12
|
+
# See https://github.com/glebm/i18n-tasks/issues/204.
|
|
13
|
+
require 'i18n/processes/scanners/pattern_mapper'
|
|
14
|
+
|
|
15
|
+
module I18n::Processes
|
|
16
|
+
module UsedKeys # rubocop:disable Metrics/ModuleLength
|
|
17
|
+
SEARCH_DEFAULTS = {
|
|
18
|
+
paths: %w[tmp/].freeze,
|
|
19
|
+
relative_roots: %w[app/controllers app/helpers app/mailers app/presenters app/views].freeze,
|
|
20
|
+
scanners: [
|
|
21
|
+
['::I18n::Processes::Scanners::RubyAstScanner', only: %w[*.rb]],
|
|
22
|
+
['::I18n::Processes::Scanners::PatternWithScopeScanner', exclude: %w[*.rb]]
|
|
23
|
+
],
|
|
24
|
+
strict: true
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
ALWAYS_EXCLUDE = %w[*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
|
|
28
|
+
*.yml *.json *.zip *.tar.gz *.swf *.flv].freeze
|
|
29
|
+
|
|
30
|
+
# Find all keys in the source and return a forest with the keys in absolute form and their occurrences.
|
|
31
|
+
#
|
|
32
|
+
# @param key_filter [String] only return keys matching this pattern.
|
|
33
|
+
# @param strict [Boolean] if true, dynamic keys are excluded (e.g. `t("category.#{ category.key }")`)
|
|
34
|
+
# @param include_raw_references [Boolean] if true, includes reference usages as they appear in the source
|
|
35
|
+
# @return [Data::Tree::Siblings]
|
|
36
|
+
def used_tree(key_filter: nil, strict: nil, include_raw_references: false)
|
|
37
|
+
src_tree = used_in_source_tree(key_filter: key_filter, strict: strict)
|
|
38
|
+
raw_refs, resolved_refs, used_refs = process_references(src_tree['used'].children)
|
|
39
|
+
raw_refs.leaves { |node| node.data[:ref_type] = :reference_usage }
|
|
40
|
+
resolved_refs.leaves { |node| node.data[:ref_type] = :reference_usage_resolved }
|
|
41
|
+
used_refs.leaves { |node| node.data[:ref_type] = :reference_usage_key }
|
|
42
|
+
src_tree.tap do |result|
|
|
43
|
+
tree = result['used'].children
|
|
44
|
+
tree.subtract_by_key!(raw_refs)
|
|
45
|
+
tree.merge!(raw_refs) if include_raw_references
|
|
46
|
+
tree.merge!(used_refs).merge!(resolved_refs)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def used_in_source_tree(key_filter: nil, strict: nil)
|
|
51
|
+
keys = ((@keys_used_in_source_tree ||= {})[strict?(strict)] ||=
|
|
52
|
+
scanner(strict: strict).keys.freeze)
|
|
53
|
+
if key_filter
|
|
54
|
+
key_filter_re = compile_key_pattern(key_filter)
|
|
55
|
+
keys = keys.select { |k| k.key =~ key_filter_re }
|
|
56
|
+
end
|
|
57
|
+
Data::Tree::Node.new(
|
|
58
|
+
key: 'used',
|
|
59
|
+
data: { key_filter: key_filter },
|
|
60
|
+
children: Data::Tree::Siblings.from_key_occurrences(keys)
|
|
61
|
+
).to_siblings
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scanner(strict: nil)
|
|
65
|
+
(@scanner ||= {})[strict?(strict)] ||= begin
|
|
66
|
+
shared_options = search_config.dup
|
|
67
|
+
shared_options.delete(:scanners)
|
|
68
|
+
shared_options[:strict] = strict unless strict.nil?
|
|
69
|
+
log_verbose 'Scanners: '
|
|
70
|
+
Scanners::ScannerMultiplexer.new(
|
|
71
|
+
scanners: search_config[:scanners].map do |(class_name, args)|
|
|
72
|
+
if args && args[:strict]
|
|
73
|
+
fail CommandError, 'the strict option is global and cannot be applied on the scanner level'
|
|
74
|
+
end
|
|
75
|
+
ActiveSupport::Inflector.constantize(class_name).new(
|
|
76
|
+
config: merge_scanner_configs(shared_options, args || {}),
|
|
77
|
+
file_finder_provider: caching_file_finder_provider,
|
|
78
|
+
file_reader: caching_file_reader
|
|
79
|
+
)
|
|
80
|
+
end.tap { |scanners| log_verbose { scanners.map { |s| " #{s.class.name} #{s.config.inspect}" } * "\n" } }
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def search_config
|
|
86
|
+
@search_config ||= begin
|
|
87
|
+
conf = (config[:search] || {}).deep_symbolize_keys
|
|
88
|
+
if conf[:scanner]
|
|
89
|
+
warn_deprecated 'search.scanner is now search.scanners, an array of [ScannerClass, options]'
|
|
90
|
+
conf[:scanners] = [[conf.delete(:scanner)]]
|
|
91
|
+
end
|
|
92
|
+
if conf[:ignore_lines]
|
|
93
|
+
warn_deprecated 'search.ignore_lines is no longer a global setting: pass it directly to the pattern scanner.'
|
|
94
|
+
conf.delete(:ignore_lines)
|
|
95
|
+
end
|
|
96
|
+
if conf[:include]
|
|
97
|
+
warn_deprecated 'search.include is now search.only'
|
|
98
|
+
conf[:only] = conf.delete(:include)
|
|
99
|
+
end
|
|
100
|
+
merge_scanner_configs(SEARCH_DEFAULTS, conf).freeze
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def merge_scanner_configs(a, b)
|
|
105
|
+
a.deep_merge(b).tap do |c|
|
|
106
|
+
%i[scanners paths relative_roots].each do |key|
|
|
107
|
+
c[key] = a[key] if b[key].blank?
|
|
108
|
+
end
|
|
109
|
+
%i[exclude].each do |key|
|
|
110
|
+
merged = Array(a[key]) + Array(b[key])
|
|
111
|
+
c[key] = merged unless merged.empty?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def caching_file_finder_provider
|
|
117
|
+
@caching_file_finder_provider ||= Scanners::Files::CachingFileFinderProvider.new(exclude: ALWAYS_EXCLUDE)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def caching_file_reader
|
|
121
|
+
@caching_file_reader ||= Scanners::Files::CachingFileReader.new
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [Boolean] whether the key is potentially used in a code expression such as `t("category.#{category_key}")`
|
|
125
|
+
def used_in_expr?(key)
|
|
126
|
+
!!(key =~ expr_key_re) # rubocop:disable Style/DoubleNegation
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# @param strict [Boolean, nil]
|
|
132
|
+
# @return [Boolean]
|
|
133
|
+
def strict?(strict)
|
|
134
|
+
strict.nil? ? search_config[:strict] : strict
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# keys in the source that end with a ., e.g. t("category.#{ cat.i18n_key }") or t("category." + category.key)
|
|
138
|
+
# @param [String] replacement for interpolated values.
|
|
139
|
+
def expr_key_re(replacement: ':')
|
|
140
|
+
@expr_key_re ||= begin
|
|
141
|
+
# disallow patterns with no keys
|
|
142
|
+
ignore_pattern_re = /\A[\.#{replacement}]*\z/
|
|
143
|
+
patterns = used_in_source_tree(strict: false).key_names.select do |k|
|
|
144
|
+
k.end_with?('.') || k =~ /\#{/
|
|
145
|
+
end.map do |k|
|
|
146
|
+
pattern = "#{replace_key_exp(k, replacement)}#{replacement if k.end_with?('.')}"
|
|
147
|
+
next if pattern =~ ignore_pattern_re
|
|
148
|
+
pattern
|
|
149
|
+
end.compact
|
|
150
|
+
compile_key_pattern "{#{patterns * ','}}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Replace interpolations in dynamic keys such as "category.#{category.i18n_key}".
|
|
155
|
+
# @param key [String]
|
|
156
|
+
# @param replacement [String]
|
|
157
|
+
# @return [String]
|
|
158
|
+
def replace_key_exp(key, replacement)
|
|
159
|
+
scanner = StringScanner.new(key)
|
|
160
|
+
braces = []
|
|
161
|
+
result = []
|
|
162
|
+
while (match_until = scanner.scan_until(/(?:#?\{|})/))
|
|
163
|
+
if scanner.matched == '#{'
|
|
164
|
+
braces << scanner.matched
|
|
165
|
+
result << match_until[0..-3] if braces.length == 1
|
|
166
|
+
elsif scanner.matched == '}'
|
|
167
|
+
prev_brace = braces.pop
|
|
168
|
+
result << replacement if braces.empty? && prev_brace == '#{'
|
|
169
|
+
else
|
|
170
|
+
braces << '{'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
result << key[scanner.pos..-1] unless scanner.eos?
|
|
174
|
+
result.join
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# define all the modules to be able to use ::
|
|
4
|
+
module I18n
|
|
5
|
+
module Processes
|
|
6
|
+
class << self
|
|
7
|
+
def gem_path
|
|
8
|
+
File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def verbose?
|
|
12
|
+
@verbose
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_writer :verbose
|
|
16
|
+
|
|
17
|
+
# Add a scanner to the default configuration.
|
|
18
|
+
#
|
|
19
|
+
# @param scanner_class_name [String]
|
|
20
|
+
# @param scanner_opts [Hash]
|
|
21
|
+
# @return self
|
|
22
|
+
def add_scanner(scanner_class_name, scanner_opts = {})
|
|
23
|
+
scanners = I18n::Processes::Configuration::DEFAULTS[:search][:scanners]
|
|
24
|
+
scanners << [scanner_class_name, scanner_opts]
|
|
25
|
+
scanners.uniq!
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add commands to i18n-processes
|
|
30
|
+
#
|
|
31
|
+
# @param commands_module [Module]
|
|
32
|
+
# @return self
|
|
33
|
+
def add_commands(commands_module)
|
|
34
|
+
::I18n::Processes::Commands.send :include, commands_module
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@verbose = !ENV['VERBOSE'].nil?
|
|
40
|
+
|
|
41
|
+
module Data
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require 'active_support/inflector'
|
|
47
|
+
require 'active_support/core_ext/hash'
|
|
48
|
+
require 'active_support/core_ext/array/access'
|
|
49
|
+
require 'active_support/core_ext/array/extract_options'
|
|
50
|
+
require 'active_support/core_ext/module/delegation'
|
|
51
|
+
require 'active_support/core_ext/object/blank'
|
|
52
|
+
begin
|
|
53
|
+
# activesupport >= 3
|
|
54
|
+
require 'active_support/core_ext/object/try'
|
|
55
|
+
rescue LoadError => _e
|
|
56
|
+
# activesupport ~> 2.3.2
|
|
57
|
+
require 'active_support/core_ext/try'
|
|
58
|
+
end
|
|
59
|
+
require 'rainbow'
|
|
60
|
+
require 'erubi'
|
|
61
|
+
|
|
62
|
+
require 'i18n/processes/version'
|
|
63
|
+
require 'i18n/processes/base_process'
|
|
64
|
+
|
|
65
|
+
# Add internal locale data to i18n gem load path
|
|
66
|
+
require 'i18n'
|
|
67
|
+
Dir[File.join(I18n::Processes.gem_path, 'config', 'locales', '*.yml')].each do |locale_file|
|
|
68
|
+
I18n.config.load_path << locale_file
|
|
69
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# page
|
|
2
|
+
company.show.title=公司设置
|
|
3
|
+
company.show.subtitle=公司基本信息
|
|
4
|
+
company.show.save-button=保存修改
|
|
5
|
+
|
|
6
|
+
company.nav.show=基本信息
|
|
7
|
+
company.nav.advance=高级设置
|
|
8
|
+
company.nav.orders=订单中心
|
|
9
|
+
|
|
10
|
+
company.show.rightbar.invation=邀请
|
|
11
|
+
company.show.rightbar.invation.link=点击邀请同事加入
|
|
12
|
+
company.show.rightbar.payinfo=付费信息
|
|
13
|
+
company.show.rightbar.buy-link=购买授权
|
|
14
|
+
company.show.rightbar.price-link=版本功能比较
|
|
15
|
+
company.show.rightbar.expired-on=有效期至
|
|
16
|
+
company.show.rightbar.max-rooms-count=授权可开通会议室数
|
|
17
|
+
company.show.rightbar.current-rooms-count=已经开通会议室数量
|
|
18
|
+
company.show.rightbar.custom-domain=自定义域名
|
|
19
|
+
company.show.rightbar.custom-domain.hint1=%s 付费用户可支持自定义域名,请发邮件至
|
|
20
|
+
company.show.rightbar.custom-domain.hint2=申请,并提供您期望映射的公司域名。
|
|
21
|
+
company.show.rightbar.custom-domain.hint3=我们将在一个工作日内和您联系,完成配置工作。
|
|
22
|
+
|
|
23
|
+
company.show.rightbar.invate-code.line1=快速提示: 可让同事用微信搜索公众号《会议室助手》
|
|
24
|
+
company.show.rightbar.invate-code.line2=然后使用公司邀请码加入:
|
|
25
|
+
|
|
26
|
+
company.advance.subtitle=公司高级设置
|
|
27
|
+
company.advance.ext-meeting-type=会议扩展类型
|
|
28
|
+
company.advance.ext-meeting-type.hint=可以取消不使用的会议类型
|
|
29
|
+
company.advance.booking-begin=会议预订开始时间
|
|
30
|
+
company.advance.booking-end=会议预订结束时间
|
|
31
|
+
company.advance.booking-limit=会议预订时间限制
|
|
32
|
+
company.advance.booking-split=会议预订时间间隔
|
|
33
|
+
company.advance.booking-date-limit=预订日期限制
|
|
34
|
+
company.advance.booking-date-limit.hint=指定只能预订指定天数内的会, 过远的日期可能导致资源浪费;固定会议亦受此限制
|
|
35
|
+
|
|
36
|
+
# form
|
|
37
|
+
company.name=公司名称
|
|
38
|
+
company.corpName=公司名称
|
|
39
|
+
company.notice=公司简介
|
|
40
|
+
company.notice.placeholder=输入公司简介
|
|
41
|
+
company.notice.hint=200字以内
|
|
42
|
+
company.emailPostfix=公司邮箱后缀
|
|
43
|
+
company.emailPostfix.placeholder=用于确保只有公司员工才可以加入,不填写则不做邮箱限制
|
|
44
|
+
company.emailPostfix.hint=是邮箱@后面的值,例如公司邮箱tom@bigcompany.com,则填写bigcompany.com;可用逗号分隔设置多个
|
|
45
|
+
company.logoFile=公司Logo图片
|
|
46
|
+
company.logoFile.hint.please-select-file=请选择公司Logo图片文件
|
|
47
|
+
company.logoFile.hint.logo-usage=可以上传公司Logo, 显示在系统界面上。请确保使用正方形图片。
|
|
48
|
+
company.logoFile.hint.remove-logo.prefix=您也可以
|
|
49
|
+
company.logoFile.hint.remove-logo.title=删除公司Logo
|
|
50
|
+
company.logoFile.hint.remove-logo.confirm=确认删除公司Logo?
|
|
51
|
+
company.logoFile.alt=当前Logo
|
|
52
|
+
corpLang=公司默认界面语言
|
|
53
|
+
company.wxqyContactsSecret=企业微信通讯录Secret
|
|
54
|
+
company.wxqyContactsSecret.placeholder=用于同步企业微信组织架构到会议室系统
|
|
55
|
+
company.wxqyContactsSecret.hint=进入企业微信管理后台,在“管理工具”—“通讯录同步助手”开启“API接口同步”,然后复制界面上的Secret到这里
|
|
56
|
+
|
|
57
|
+
# flash
|
|
58
|
+
flash.company.notice.save-advance-succ=公司高级设置保存成功!
|
|
59
|
+
flash.company.notice.rm-logo-succ=删除公司Logo成功!
|
|
60
|
+
flash.company.warn.bad-time-split=时间间隔非法!
|
|
61
|
+
flash.company.warn.need-square-picture=请确保选择正方形的图片!
|
|
62
|
+
flash.company.warn.edu-cannot-change-name=您的授权不允许修改公司名称,已恢复原名称,有需要可联系客服修改
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
deviceType.ANDROID_PAD=原生Android屏
|
|
2
|
+
deviceType.H5_PAD=显示屏
|
|
3
|
+
deviceType.LIGHT=灯控
|
|
4
|
+
deviceType.POWER=电源控制
|
|
5
|
+
deviceType.DOOR=门禁
|
|
6
|
+
deviceType.HUMAN_INFRARED=人体红外探测
|
|
7
|
+
deviceType.HUMAN_DETECT=人体活动探测
|
|
8
|
+
deviceType.AGENT=设备代理
|
|
9
|
+
|
|
10
|
+
deviceInstruct.ANDROID_PAD=原生Android屏
|
|
11
|
+
deviceInstruct.H5_PAD=显示屏
|
|
12
|
+
deviceInstruct.LIGHT=灯光
|
|
13
|
+
deviceInstruct.LIGHT.true=开
|
|
14
|
+
deviceInstruct.LIGHT.false=关
|
|
15
|
+
deviceInstruct.LIGHT.null=未连接
|
|
16
|
+
deviceInstruct.POWER=电源
|
|
17
|
+
deviceInstruct.POWER.true=开
|
|
18
|
+
deviceInstruct.POWER.false=关
|
|
19
|
+
deviceInstruct.POWER.null=未连接
|
|
20
|
+
deviceInstruct.DOOR=门禁
|
|
21
|
+
deviceInstruct.DOOR.true=开
|
|
22
|
+
deviceInstruct.DOOR.false=关
|
|
23
|
+
deviceInstruct.DOOR.null=未连接
|
|
24
|
+
deviceInstruct.HUMAN_INFRARED=人体探测
|
|
25
|
+
deviceInstruct.HUMAN_INFRARED.true=有人
|
|
26
|
+
deviceInstruct.HUMAN_INFRARED.false=无人
|
|
27
|
+
deviceInstruct.HUMAN_INFRARED.null=未连接
|
|
28
|
+
deviceInstruct.HUMAN_DETECT=人体探测
|
|
29
|
+
deviceInstruct.HUMAN_DETECT.true=有人
|
|
30
|
+
deviceInstruct.HUMAN_DETECT.false=无人
|
|
31
|
+
deviceInstruct.HUMAN_DETECT.null=未连接
|
|
32
|
+
deviceInstruct.AGENT=连接
|
|
33
|
+
deviceInstruct.AGENT.true=在线
|
|
34
|
+
deviceInstruct.AGENT.false=离线
|
|
35
|
+
deviceInstruct.AGENT.null=未连接
|
|
36
|
+
|
|
37
|
+
# 界面
|
|
38
|
+
manager.devices.unbind=解除绑定
|
|
39
|
+
manager.devices.bind=绑定
|
|
40
|
+
manager.devices.confirm-unbind=确认解除%s的绑定?
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# index页面元素
|
|
2
|
+
meeting-rooms.index.title=会议室管理
|
|
3
|
+
meeting-rooms.index.subtitle=维护公司可用的会议室
|
|
4
|
+
meeting-rooms.index.tablabel=会议室
|
|
5
|
+
meeting-rooms.index.drag-tips=拖动会议室可进行排序
|
|
6
|
+
meeting-rooms.operate.new=新增会议室
|
|
7
|
+
|
|
8
|
+
meeting-rooms.index.open-for-view=开放会议查看
|
|
9
|
+
meeting-rooms.index.open-for-view.hint=非注册用户,可以查看会议室安排
|
|
10
|
+
|
|
11
|
+
meeting-rooms.operate.print=打印二维码门牌
|
|
12
|
+
|
|
13
|
+
meeting-rooms.add.subtitle=添加新会议室
|
|
14
|
+
|
|
15
|
+
meeting-rooms.edit.subtitle=修改 - %s
|
|
16
|
+
meeting-rooms.pad.title=绑定Pad设备
|
|
17
|
+
meeting-rooms.pad.had-binded=当前会议室已经绑定了Pad设备。
|
|
18
|
+
meeting-rooms.pad.rebind=重新绑定Pad设备
|
|
19
|
+
meeting-rooms.pad.install-app-please=请安装Android App:
|
|
20
|
+
meeting-rooms.pad.open-and-input-bindcode=打开App后, 按提示输入绑定码:
|
|
21
|
+
|
|
22
|
+
meeting-rooms.show.top-hint1=临时使用会议室,请用微信扫一扫预订
|
|
23
|
+
meeting-rooms.show.top-hint2=以免与他人时间冲突
|
|
24
|
+
meeting-rooms.show.bottom-hint1=本页可直接打印,张贴在会议室门口,方便用微信扫一扫查看会议室状态
|
|
25
|
+
meeting-rooms.show.bottom-hint2=『打印』按钮之后的内容不会被打印
|
|
26
|
+
meeting-rooms.show.bottom-hint3=您也可以复制以上内容到Word编辑后再打印
|
|
27
|
+
|
|
28
|
+
meeting-rooms.device.title=会议室设备
|
|
29
|
+
|
|
30
|
+
# MeetingRoom Form
|
|
31
|
+
meetingRoom.name=会议室名称
|
|
32
|
+
meetingRoom.name.placeholder=请输入会议室名称
|
|
33
|
+
meetingRoom.location=位置
|
|
34
|
+
meetingRoom.location.placeholder=会议室位置
|
|
35
|
+
meetingRoom.location.hint=简单说明会议室所在的位置, 如『三号楼西侧』, 让大家可以知道在哪
|
|
36
|
+
meetingRoom.capacity=可容纳人数
|
|
37
|
+
meetingRoom.capacity.placeholder=请输入会议室可容纳人数
|
|
38
|
+
meetingRoom.facilities=会议设施
|
|
39
|
+
meetingRoom.facilities.hint=此会议室可用的会议设施
|
|
40
|
+
meetingRoom.officeArea=办公区域
|
|
41
|
+
disabledReason=禁用原因
|
|
42
|
+
meetingRoom.disableReason.placeholder=请用简短文字说明会议室禁用的原因
|
|
43
|
+
meetingRoom.disableReason.hint=50字以内, 将用于显示在微信会议室查看界面, 提示相关人员
|
|
44
|
+
|
|
45
|
+
meetingRoom.pricePerHour=每小时费用
|
|
46
|
+
|
|
47
|
+
meetingRoom.shortName=短名称
|
|
48
|
+
meetingRoom.shortName.placeholder=请输入会议室的简短名称,建议不超过10字,可不输入
|
|
49
|
+
meetingRoom.shortName.hint=短名称用于显示在会议通知、显示屏等有空间要求的地方;不输入则直接使用『名称』
|
|
50
|
+
|
|
51
|
+
meetingRoom.allowRoles=预订许可
|
|
52
|
+
meetingRoom.allowRoles.hint=如不勾选"所有人可预订", 则普通员工不能预订此会议室, 只有指定角色可以预定
|
|
53
|
+
|
|
54
|
+
meetingRoom.settings=会议室设置
|
|
55
|
+
meetingRoom.settings.not-public=不对外公开会议安排
|
|
56
|
+
meetingRoom.settings.not-public.hint=非注册用户,不能查看会议室安排
|
|
57
|
+
|
|
58
|
+
meetingRoom.notice=会议室注意事项
|
|
59
|
+
meetingRoom.notice.placeholder=输入会议室注意事项
|
|
60
|
+
meetingRoom.notice.hint=可选, 200字以内, 告知此会议室使用注意事项
|
|
61
|
+
|
|
62
|
+
meetingRoom.officeArea.id=办公区域
|
|
63
|
+
meetingRoom.officeArea.hint=可选, 地理位置区分的办公区域, 可以认为一个"前台接待工位"对应一个办公区域
|
|
64
|
+
|
|
65
|
+
meetingRoom.disable-for-short-time-no-reason=无理由,暂时禁用
|
|
66
|
+
meetingRoom.disable-for-short-time=暂时禁用
|
|
67
|
+
meetingRoom.disable-for-short-time.hint=暂时不允许预订
|
|
68
|
+
meetingRoom.booking.allowAll=所有人可预订
|
|
69
|
+
meetingRoom.capacity.number=可容纳%s人
|
|
70
|
+
|
|
71
|
+
# 限#{list room?.allowRoleSet(), as:'role'}&{'roles.' + role}#{if !role_isLast}、#{/if}#{/list}预订
|
|
72
|
+
meetingRoom.booking.limitFor.start=限
|
|
73
|
+
meetingRoom.booking.limitFor.end=预订
|
|
74
|
+
|
|
75
|
+
# flash
|
|
76
|
+
flash.notice.create-rooms-succ=会议室%s创建成功
|
|
77
|
+
flash.notice.save-rooms-succ=会议室%s修改成功
|
|
78
|
+
flash.notice.disable-rooms-succ=会议室%s禁用成功
|
|
79
|
+
flash.notice.disable-rooms-with-meetings=会议室%s禁用成功, 但还有%s个会议, 请线下通知相关人员
|
|
80
|
+
flash.notice.meeting-rooms.generate-pad-bind-key-succ=已经重新生成Pad绑定码, 请到Pad界面输入新的绑定码
|
|
81
|
+
flash.warn.meeting-room-deleted-with-meetings=%s还有会议安排,请先确保%s无会议安排后再删除会议室。
|
|
82
|
+
flash.warn.meeting-room-reach-max-count=已经达到允许可创建会议室数量上限(%s间), 不能再新增会议室, 请联系在线客服购买更多会议室许可
|
|
83
|
+
|
|
84
|
+
flash.notice.meeting-rooms.unbind-device-succ=解绑设备%s(编号:%s)成功
|
|
85
|
+
|
|
86
|
+
# validation
|
|
87
|
+
validate.meetingRoom.disableReason.require=禁用原因必填
|
|
88
|
+
validate.meetingRoom.disableReason.length=禁用原因不能超过50字
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# 其它
|
|
92
|
+
meeting-rooms.bookingPermision=预订权限
|
|
93
|
+
meeting-rooms.not-exists=不存在的会议室
|
|
94
|
+
meeting-rooms.not-bind-door=没有绑定门禁设备
|
|
95
|
+
meeting-rooms.not-bind-light=没有绑定灯光控制设备
|
|
96
|
+
meeting-rooms.not-bind-power=没有绑定电源控制设备
|
|
97
|
+
|
|
98
|
+
# 交易
|
|
99
|
+
trade.changeAmount=交易金额
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# HTML页面
|
|
2
|
+
meetingBooking.title.main=预订会议
|
|
3
|
+
meetingBooking.title.booking-by-date=按日期预订
|
|
4
|
+
meetingBooking.title.detail=会议详情
|
|
5
|
+
meetingBooking.title.find-room-by-time=按时间找会议室
|
|
6
|
+
meetingBooking.title.find-time-by-room=按会议室找时间
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
meetingBooking.booking=预订
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 确认状态
|
|
13
|
+
meeting.joinStatus.notMember=非会议参与人员不能设置参与状态
|
|
14
|
+
meeting.joinStatus.invalidStatus=非法参与状态
|
|
15
|
+
|
|
16
|
+
# 会议审批
|
|
17
|
+
meetingApprove.approval=是否通过
|
|
18
|
+
meetingApprove.remark=备注
|