i18n-processes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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=备注
|