i18n-processes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +102 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +12 -0
  6. data/bin/i18n-processes +28 -0
  7. data/bin/i18n-processes.cmd +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/config/locales/zh-CN.yml +2 -0
  10. data/i18n-processes.gemspec +64 -0
  11. data/lib/i18n/processes/base_process.rb +47 -0
  12. data/lib/i18n/processes/cli.rb +208 -0
  13. data/lib/i18n/processes/command/collection.rb +21 -0
  14. data/lib/i18n/processes/command/commander.rb +43 -0
  15. data/lib/i18n/processes/command/commands/data.rb +107 -0
  16. data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
  17. data/lib/i18n/processes/command/commands/health.rb +26 -0
  18. data/lib/i18n/processes/command/commands/meta.rb +38 -0
  19. data/lib/i18n/processes/command/commands/missing.rb +86 -0
  20. data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
  21. data/lib/i18n/processes/command/commands/tree.rb +119 -0
  22. data/lib/i18n/processes/command/commands/usages.rb +69 -0
  23. data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
  24. data/lib/i18n/processes/command/dsl.rb +56 -0
  25. data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
  26. data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
  27. data/lib/i18n/processes/command/options/common.rb +41 -0
  28. data/lib/i18n/processes/command/options/data.rb +95 -0
  29. data/lib/i18n/processes/command/options/locales.rb +36 -0
  30. data/lib/i18n/processes/command_error.rb +13 -0
  31. data/lib/i18n/processes/commands.rb +31 -0
  32. data/lib/i18n/processes/configuration.rb +132 -0
  33. data/lib/i18n/processes/console_context.rb +76 -0
  34. data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
  35. data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
  36. data/lib/i18n/processes/data/file_formats.rb +111 -0
  37. data/lib/i18n/processes/data/file_system.rb +14 -0
  38. data/lib/i18n/processes/data/file_system_base.rb +205 -0
  39. data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
  40. data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
  41. data/lib/i18n/processes/data/tree/node.rb +204 -0
  42. data/lib/i18n/processes/data/tree/nodes.rb +97 -0
  43. data/lib/i18n/processes/data/tree/siblings.rb +333 -0
  44. data/lib/i18n/processes/data/tree/traversal.rb +190 -0
  45. data/lib/i18n/processes/data.rb +87 -0
  46. data/lib/i18n/processes/google_translation.rb +125 -0
  47. data/lib/i18n/processes/html_keys.rb +16 -0
  48. data/lib/i18n/processes/ignore_keys.rb +30 -0
  49. data/lib/i18n/processes/key_pattern_matching.rb +37 -0
  50. data/lib/i18n/processes/locale_list.rb +19 -0
  51. data/lib/i18n/processes/locale_pathname.rb +17 -0
  52. data/lib/i18n/processes/logging.rb +37 -0
  53. data/lib/i18n/processes/missing_keys.rb +122 -0
  54. data/lib/i18n/processes/path.rb +42 -0
  55. data/lib/i18n/processes/plural_keys.rb +41 -0
  56. data/lib/i18n/processes/rainbow_utils.rb +13 -0
  57. data/lib/i18n/processes/references.rb +101 -0
  58. data/lib/i18n/processes/reports/base.rb +71 -0
  59. data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
  60. data/lib/i18n/processes/reports/terminal.rb +252 -0
  61. data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
  62. data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
  63. data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
  64. data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
  65. data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
  66. data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
  67. data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
  68. data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
  69. data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
  70. data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
  71. data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
  72. data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
  73. data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
  74. data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
  75. data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
  76. data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
  77. data/lib/i18n/processes/scanners/scanner.rb +17 -0
  78. data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
  79. data/lib/i18n/processes/split_key.rb +68 -0
  80. data/lib/i18n/processes/stats.rb +24 -0
  81. data/lib/i18n/processes/string_interpolation.rb +16 -0
  82. data/lib/i18n/processes/unused_keys.rb +23 -0
  83. data/lib/i18n/processes/used_keys.rb +177 -0
  84. data/lib/i18n/processes/version.rb +7 -0
  85. data/lib/i18n/processes.rb +69 -0
  86. data/source/p1/_messages/zh/article.properties +9 -0
  87. data/source/p1/_messages/zh/company.properties +62 -0
  88. data/source/p1/_messages/zh/devices.properties +40 -0
  89. data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
  90. data/source/p1/_messages/zh/meetingBooking.properties +18 -0
  91. data/source/p1/_messages/zh/office-areas.properties +64 -0
  92. data/source/p1/_messages/zh/orders.properties +25 -0
  93. data/source/p1/_messages/zh/schedulings.properties +7 -0
  94. data/source/p1/_messages/zh/tag.properties +2 -0
  95. data/source/p1/_messages/zh/ticket.properties +9 -0
  96. data/source/p1/_messages/zh/visitor.properties +5 -0
  97. data/source/p1/messages +586 -0
  98. data/source/p2/orders.properties +25 -0
  99. data/source/p2/schedulings.properties +7 -0
  100. data/source/p2/tag.properties +2 -0
  101. data/source/p2/ticket.properties +9 -0
  102. data/source/p2/visitor.properties +5 -0
  103. data/source/zh.messages.ts +30 -0
  104. data/translated/en/p1/_messages/zh/article.properties +9 -0
  105. data/translated/en/p1/_messages/zh/company.properties +62 -0
  106. data/translated/en/p1/_messages/zh/devices.properties +40 -0
  107. data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
  108. data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
  109. data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
  110. data/translated/en/p1/_messages/zh/orders.properties +25 -0
  111. data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
  112. data/translated/en/p1/_messages/zh/tag.properties +2 -0
  113. data/translated/en/p1/_messages/zh/ticket.properties +9 -0
  114. data/translated/en/p1/_messages/zh/visitor.properties +5 -0
  115. data/translated/en/p1/messages +586 -0
  116. data/translated/en/p2/orders.properties +25 -0
  117. data/translated/en/p2/schedulings.properties +7 -0
  118. data/translated/en/p2/tag.properties +2 -0
  119. data/translated/en/p2/ticket.properties +9 -0
  120. data/translated/en/p2/visitor.properties +5 -0
  121. data/translated/en/zh.messages.ts +30 -0
  122. data/translation/en/article.properties +9 -0
  123. data/translation/en/company.properties +56 -0
  124. data/translation/en/meeting-rooms.properties +87 -0
  125. data/translation/en/meetingBooking.properties +14 -0
  126. data/translation/en/messages.en +164 -0
  127. data/translation/en/office-areas.properties +51 -0
  128. data/translation/en/orders.properties +26 -0
  129. data/translation/en/tag.properties +2 -0
  130. data/translation/en/translated +1263 -0
  131. data/translation/en/visitor.properties +4 -0
  132. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Processes
5
+ VERSION = '0.1.0'
6
+ end
7
+ 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,9 @@
1
+ # 文章
2
+ article.title=标题
3
+ article.shown=是否显示
4
+ article.showOrder=显示顺序(倒序)
5
+ article.channel=频道
6
+ article.category=文章栏目
7
+ article.category.id=文章栏目
8
+ article.friendlyName=英文名称
9
+ article.readCount=阅读数
@@ -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=备注