rosette-core 1.0.1

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.
Files changed (158) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +26 -0
  3. data/History.txt +3 -0
  4. data/README.md +94 -0
  5. data/Rakefile +18 -0
  6. data/lib/rosette/core.rb +110 -0
  7. data/lib/rosette/core/branch_utils.rb +152 -0
  8. data/lib/rosette/core/commands.rb +139 -0
  9. data/lib/rosette/core/commands/errors.rb +17 -0
  10. data/lib/rosette/core/commands/git/commit_command.rb +65 -0
  11. data/lib/rosette/core/commands/git/diff_base_command.rb +301 -0
  12. data/lib/rosette/core/commands/git/diff_command.rb +188 -0
  13. data/lib/rosette/core/commands/git/diff_entry.rb +44 -0
  14. data/lib/rosette/core/commands/git/fetch_command.rb +27 -0
  15. data/lib/rosette/core/commands/git/repo_snapshot_command.rb +40 -0
  16. data/lib/rosette/core/commands/git/show_command.rb +70 -0
  17. data/lib/rosette/core/commands/git/snapshot_command.rb +50 -0
  18. data/lib/rosette/core/commands/git/status_command.rb +128 -0
  19. data/lib/rosette/core/commands/git/with_non_merge_ref.rb +48 -0
  20. data/lib/rosette/core/commands/git/with_ref.rb +92 -0
  21. data/lib/rosette/core/commands/git/with_refs.rb +92 -0
  22. data/lib/rosette/core/commands/git/with_repo_name.rb +50 -0
  23. data/lib/rosette/core/commands/git/with_snapshots.rb +45 -0
  24. data/lib/rosette/core/commands/queuing/enqueue_commit_command.rb +37 -0
  25. data/lib/rosette/core/commands/queuing/requeue_commit_command.rb +46 -0
  26. data/lib/rosette/core/commands/translations/export_command.rb +257 -0
  27. data/lib/rosette/core/commands/translations/translation_lookup_command.rb +66 -0
  28. data/lib/rosette/core/commands/translations/with_locale.rb +47 -0
  29. data/lib/rosette/core/configurator.rb +160 -0
  30. data/lib/rosette/core/error_reporters/buffered_error_reporter.rb +96 -0
  31. data/lib/rosette/core/error_reporters/error_reporter.rb +31 -0
  32. data/lib/rosette/core/error_reporters/nil_error_reporter.rb +25 -0
  33. data/lib/rosette/core/error_reporters/printing_error_reporter.rb +58 -0
  34. data/lib/rosette/core/error_reporters/raising_error_reporter.rb +27 -0
  35. data/lib/rosette/core/errors.rb +93 -0
  36. data/lib/rosette/core/extractor/commit_log.rb +33 -0
  37. data/lib/rosette/core/extractor/commit_log_status.rb +57 -0
  38. data/lib/rosette/core/extractor/commit_processor.rb +109 -0
  39. data/lib/rosette/core/extractor/extractor.rb +72 -0
  40. data/lib/rosette/core/extractor/extractor_config.rb +74 -0
  41. data/lib/rosette/core/extractor/locale.rb +118 -0
  42. data/lib/rosette/core/extractor/phrase.rb +76 -0
  43. data/lib/rosette/core/extractor/phrase/phrase_index_policy.rb +108 -0
  44. data/lib/rosette/core/extractor/phrase/phrase_to_hash.rb +33 -0
  45. data/lib/rosette/core/extractor/repo_config.rb +339 -0
  46. data/lib/rosette/core/extractor/serializer_config.rb +55 -0
  47. data/lib/rosette/core/extractor/static_extractor.rb +44 -0
  48. data/lib/rosette/core/extractor/translation.rb +44 -0
  49. data/lib/rosette/core/extractor/translation/translation_to_hash.rb +28 -0
  50. data/lib/rosette/core/git/diff_finder.rb +131 -0
  51. data/lib/rosette/core/git/ref.rb +116 -0
  52. data/lib/rosette/core/git/repo.rb +378 -0
  53. data/lib/rosette/core/path_matcher_factory.rb +330 -0
  54. data/lib/rosette/core/resolvers/extractor_id.rb +37 -0
  55. data/lib/rosette/core/resolvers/integration_id.rb +37 -0
  56. data/lib/rosette/core/resolvers/preprocessor_id.rb +38 -0
  57. data/lib/rosette/core/resolvers/resolver.rb +115 -0
  58. data/lib/rosette/core/resolvers/serializer_id.rb +37 -0
  59. data/lib/rosette/core/snapshots/cached_head_snapshot_factory.rb +51 -0
  60. data/lib/rosette/core/snapshots/cached_snapshot_factory.rb +67 -0
  61. data/lib/rosette/core/snapshots/head_snapshot_factory.rb +58 -0
  62. data/lib/rosette/core/snapshots/repo_config_path_filter.rb +83 -0
  63. data/lib/rosette/core/snapshots/snapshot_factory.rb +184 -0
  64. data/lib/rosette/core/string_utils.rb +23 -0
  65. data/lib/rosette/core/translation_status.rb +81 -0
  66. data/lib/rosette/core/validators.rb +18 -0
  67. data/lib/rosette/core/validators/commit_validator.rb +62 -0
  68. data/lib/rosette/core/validators/commits_validator.rb +32 -0
  69. data/lib/rosette/core/validators/encoding_validator.rb +32 -0
  70. data/lib/rosette/core/validators/locale_validator.rb +37 -0
  71. data/lib/rosette/core/validators/repo_validator.rb +33 -0
  72. data/lib/rosette/core/validators/serializer_validator.rb +37 -0
  73. data/lib/rosette/core/validators/validator.rb +31 -0
  74. data/lib/rosette/core/version.rb +8 -0
  75. data/lib/rosette/data_stores.rb +11 -0
  76. data/lib/rosette/data_stores/errors.rb +26 -0
  77. data/lib/rosette/data_stores/phrase_status.rb +59 -0
  78. data/lib/rosette/integrations.rb +12 -0
  79. data/lib/rosette/integrations/errors.rb +15 -0
  80. data/lib/rosette/integrations/integratable.rb +58 -0
  81. data/lib/rosette/integrations/integration.rb +23 -0
  82. data/lib/rosette/preprocessors.rb +11 -0
  83. data/lib/rosette/preprocessors/errors.rb +14 -0
  84. data/lib/rosette/preprocessors/preprocessor.rb +48 -0
  85. data/lib/rosette/queuing.rb +14 -0
  86. data/lib/rosette/queuing/commits.rb +19 -0
  87. data/lib/rosette/queuing/commits/commit_conductor.rb +90 -0
  88. data/lib/rosette/queuing/commits/commit_job.rb +93 -0
  89. data/lib/rosette/queuing/commits/commits_queue_configurator.rb +60 -0
  90. data/lib/rosette/queuing/commits/extract_stage.rb +46 -0
  91. data/lib/rosette/queuing/commits/fetch_stage.rb +51 -0
  92. data/lib/rosette/queuing/commits/finalize_stage.rb +76 -0
  93. data/lib/rosette/queuing/commits/phrase_storage_granularity.rb +20 -0
  94. data/lib/rosette/queuing/commits/push_stage.rb +91 -0
  95. data/lib/rosette/queuing/commits/stage.rb +96 -0
  96. data/lib/rosette/queuing/job.rb +74 -0
  97. data/lib/rosette/queuing/queue.rb +28 -0
  98. data/lib/rosette/queuing/queue_configurator.rb +76 -0
  99. data/lib/rosette/queuing/worker.rb +30 -0
  100. data/lib/rosette/serializers.rb +10 -0
  101. data/lib/rosette/serializers/serializer.rb +98 -0
  102. data/lib/rosette/tms.rb +9 -0
  103. data/lib/rosette/tms/repository.rb +95 -0
  104. data/rosette-core.gemspec +24 -0
  105. data/spec/core/branch_utils_spec.rb +110 -0
  106. data/spec/core/commands/git/commit_command_spec.rb +60 -0
  107. data/spec/core/commands/git/diff_command_spec.rb +263 -0
  108. data/spec/core/commands/git/fetch_command_spec.rb +61 -0
  109. data/spec/core/commands/git/repo_snapshot_command_spec.rb +72 -0
  110. data/spec/core/commands/git/show_command_spec.rb +128 -0
  111. data/spec/core/commands/git/snapshot_command_spec.rb +86 -0
  112. data/spec/core/commands/git/status_command_spec.rb +154 -0
  113. data/spec/core/commands/queuing/enqueue_commit_command_spec.rb +34 -0
  114. data/spec/core/commands/queuing/requeue_commit_command_spec.rb +46 -0
  115. data/spec/core/commands/translations/export_command_spec.rb +113 -0
  116. data/spec/core/commands/translations/translation_lookup_command_spec.rb +58 -0
  117. data/spec/core/configurator_spec.rb +47 -0
  118. data/spec/core/error_reporters/buffered_error_reporter_spec.rb +61 -0
  119. data/spec/core/error_reporters/nil_error_reporter_spec.rb +16 -0
  120. data/spec/core/error_reporters/printing_error_reporter_spec.rb +60 -0
  121. data/spec/core/extractor/commit_log_status_spec.rb +216 -0
  122. data/spec/core/extractor/commit_processor_spec.rb +68 -0
  123. data/spec/core/extractor/extractor_config_spec.rb +47 -0
  124. data/spec/core/extractor/extractor_spec.rb +26 -0
  125. data/spec/core/extractor/locale_spec.rb +92 -0
  126. data/spec/core/extractor/phrase/phrase_index_policy_spec.rb +116 -0
  127. data/spec/core/extractor/phrase/phrase_to_hash_spec.rb +18 -0
  128. data/spec/core/extractor/repo_config_spec.rb +147 -0
  129. data/spec/core/extractor/translation/translation_to_hash_spec.rb +25 -0
  130. data/spec/core/git/diff_finder_spec.rb +74 -0
  131. data/spec/core/git/ref_spec.rb +118 -0
  132. data/spec/core/git/repo_spec.rb +216 -0
  133. data/spec/core/path_matcher_factory_spec.rb +139 -0
  134. data/spec/core/resolvers/extractor_id_spec.rb +47 -0
  135. data/spec/core/resolvers/integration_id_spec.rb +47 -0
  136. data/spec/core/resolvers/preprocessor_id_spec.rb +47 -0
  137. data/spec/core/resolvers/serializer_id_spec.rb +47 -0
  138. data/spec/core/snapshots/snapshot_factory_spec.rb +145 -0
  139. data/spec/core/string_utils_spec.rb +19 -0
  140. data/spec/core/translation_status_spec.rb +91 -0
  141. data/spec/core/validators/commit_validator_spec.rb +40 -0
  142. data/spec/core/validators/encoding_validator_spec.rb +30 -0
  143. data/spec/core/validators/locale_validator_spec.rb +31 -0
  144. data/spec/core/validators/repo_validator_spec.rb +30 -0
  145. data/spec/core/validators/serializer_validator_spec.rb +31 -0
  146. data/spec/integrations/integratable_spec.rb +58 -0
  147. data/spec/queuing/commits/commit_conductor_spec.rb +71 -0
  148. data/spec/queuing/commits/commit_job_spec.rb +87 -0
  149. data/spec/queuing/commits/extract_stage_spec.rb +68 -0
  150. data/spec/queuing/commits/fetch_stage_spec.rb +101 -0
  151. data/spec/queuing/commits/finalize_stage_spec.rb +88 -0
  152. data/spec/queuing/commits/push_stage_spec.rb +145 -0
  153. data/spec/queuing/commits/stage_spec.rb +80 -0
  154. data/spec/queuing/job_spec.rb +33 -0
  155. data/spec/queuing/queue_configurator_spec.rb +44 -0
  156. data/spec/spec_helper.rb +90 -0
  157. data/spec/test_helpers/fake_commit_stage.rb +17 -0
  158. metadata +257 -0
@@ -0,0 +1,118 @@
1
+ # encoding: UTF-8
2
+
3
+ module Rosette
4
+ module Core
5
+
6
+ # Raised when locale parsing fails.
7
+ class InvalidLocaleError < StandardError; end
8
+
9
+ # Base class for representing locales. Locales are defined as a combination
10
+ # of language and territory. For example, the BCP-47 locale code for
11
+ # English from the United States is "en-US". The first part, "en", stands
12
+ # for "English", while the second part, "US", stands for "United States".
13
+ #
14
+ # @!attribute [r] language
15
+ # @return [String] the locale's language component.
16
+ # @!attribute [r] territory
17
+ # @return [String] the locale's territory component.
18
+ class Locale
19
+ # The default locale format. A locale format defines how a locale code
20
+ # should be formatted. There are a number of formats, including BCP-47,
21
+ # ISO-639-1, ISO-639-2, etc.
22
+ DEFAULT_FORMAT = :bcp_47
23
+
24
+ class << self
25
+ # Using the given format, separate the locale code into language and
26
+ # territory.
27
+ #
28
+ # @param [String] locale_code The locale code to parse.
29
+ # @param [Symbol] format The format of the locale.
30
+ # @return [Locale]
31
+ def parse(locale_code, format = DEFAULT_FORMAT)
32
+ format_str = "#{StringUtils.camelize(format.to_s)}Locale"
33
+
34
+ if Rosette::Core.const_defined?(format_str)
35
+ Rosette::Core.const_get(format_str).parse(locale_code)
36
+ else
37
+ raise ArgumentError, "locale format '#{format}' wasn't recognized"
38
+ end
39
+ end
40
+ end
41
+
42
+ attr_reader :language, :territory
43
+
44
+ # Creates a new locale.
45
+ #
46
+ # @param [String] language the locale's language component.
47
+ # @param [nil, String] territory The locale's territory component.
48
+ def initialize(language, territory = nil)
49
+ @language = language
50
+ @territory = territory
51
+ after_initialize
52
+ end
53
+
54
+ # Determines if this locale is equal to another. In order for locales
55
+ # to be equal, both the language and territory must be equal. This method
56
+ # ignores casing.
57
+ #
58
+ # @param [Locale] other The locale to compare to this one.
59
+ # @return [Boolean] true if +other+ and this locale are equivalent, false
60
+ # otherwise.
61
+ def eql?(other)
62
+ other.is_a?(self.class) &&
63
+ downcase(other.language) == downcase(language) &&
64
+ downcase(other.territory) == downcase(territory)
65
+ end
66
+
67
+ # A synonym for {#eql?}.
68
+ #
69
+ # @param [Locale] other
70
+ # @return [Boolean]
71
+ def ==(other)
72
+ eql?(other)
73
+ end
74
+
75
+ private
76
+
77
+ def after_initialize; end
78
+
79
+ def downcase(str)
80
+ (str || '').downcase
81
+ end
82
+ end
83
+
84
+ # Represents a locale in the BCP-47 format.
85
+ class Bcp47Locale < Locale
86
+ class << self
87
+ # Separates the locale code into langauge and territory components.
88
+ #
89
+ # @param [String] locale_code The locale code to parse.
90
+ # @return [Locale]
91
+ def parse(locale_code)
92
+ if valid?(locale_code)
93
+ new(*locale_code.split(/[-_]/))
94
+ else
95
+ raise InvalidLocaleError, "'#{locale_code}' is not a valid BCP-47 locale"
96
+ end
97
+ end
98
+
99
+ # Determines if the given locale code is a valid BCP-47 locale.
100
+ #
101
+ # @param [String] locale_code The locale code to validate.
102
+ # @return [Boolean] true if +locale_code+ is a valid BCP-47 locale,
103
+ # false otherwise.
104
+ def valid?(locale_code)
105
+ !!(locale_code =~ /\A[a-zA-Z]{2,4}(?:[-_][a-zA-Z0-9]{2,5})?\z/)
106
+ end
107
+ end
108
+
109
+ # Constructs a string locale code from the language and territory components.
110
+ #
111
+ # @return [String] the language and territory separated by a dash.
112
+ def code
113
+ territory ? language + "-#{territory}" : language
114
+ end
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: UTF-8
2
+
3
+ module Rosette
4
+ module Core
5
+
6
+ # Represents a phrase. Phrases are essentially text in some source
7
+ # language.
8
+ #
9
+ # @!attribute [r] key
10
+ # @return [String] the phrase key.
11
+ # @!attribute [r] meta_key
12
+ # @return [String] the phrase meta key.
13
+ # @!attribute [rw] file
14
+ # @return [String] the file this phrase was found in.
15
+ # @!attribute [rw] commit_id
16
+ # @return [String] the git commit id this phrase was found in.
17
+ # @!attribute [rw] author_name
18
+ # @return [String] the name of the person who git thinks created
19
+ # this phrase.
20
+ # @!attribute [rw] author_email
21
+ # @return [String] the email address of the person who git thinks
22
+ # created this phrase.
23
+ # @!attribute [rw] line_number
24
+ # @return [Fixnum] the line number in +file+ were this phrase was
25
+ # found.
26
+ class Phrase
27
+ include PhraseIndexPolicy
28
+ include PhraseToHash
29
+
30
+ attr_reader :key, :meta_key
31
+ attr_accessor :file, :commit_id
32
+ attr_accessor :author_name, :author_email
33
+ attr_accessor :line_number
34
+
35
+ # Creates a new phrase.
36
+ #
37
+ # @param [String] key The phrase key.
38
+ # @param [String] meta_key The phrase meta key.
39
+ # @param [String] file The file this phrase was found in.
40
+ # @param [String] commit_id The git commit id this phrase was found in.
41
+ # @param [String] author_name The name of the person who git thinks
42
+ # created this phrase.
43
+ # @param [String] author_email The email address of the person who git
44
+ # thinks created this phrase.
45
+ # @param [Fixnum] line_number The line number in +file+ were this phrase
46
+ # was found.
47
+ def initialize(key, meta_key = nil, file = nil, commit_id = nil, author_name = nil, author_email = nil, line_number = nil)
48
+ @key = key
49
+ @meta_key = meta_key
50
+ @file = file
51
+ @commit_id = commit_id
52
+ @author_name = author_name
53
+ @author_email = author_email
54
+ @line_number = line_number
55
+ end
56
+
57
+ # Creates a phrase from a hash of attributes.
58
+ #
59
+ # @param [Hash] hash A hash of options containing +key+, +meta_key+,
60
+ # +file+, +commit_id+, +author_name+, +author_email+, and +line_number+.
61
+ # @return [nil, Phrase] a phrase object created from +hash+. If +hash+
62
+ # is +nil+, returns +nil+.
63
+ def self.from_h(hash)
64
+ if hash
65
+ new(
66
+ hash[:key], hash[:meta_key],
67
+ hash[:file], hash[:commit_id],
68
+ hash[:author_name], hash[:author_email],
69
+ hash[:line_number]
70
+ )
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: UTF-8
2
+
3
+ module Rosette
4
+ module Core
5
+
6
+ # Defines the logic for which field should be used to index a phrase.
7
+ # Phrases can be indexed either by key or meta key. The logic in this
8
+ # module is designed to determine which of these fields to use for a
9
+ # particular phrase, taking into consideration +nil+ and blank values.
10
+ # By default, phrases are indexed by meta key. If however a phrase has
11
+ # a +nil+ or blank meta key, the key should be used as the index value
12
+ # instead.
13
+ #
14
+ # Must be mixed into an object that responds to {Phrase} methods,
15
+ # specifically +key+ and +meta_key+.
16
+ #
17
+ # @example
18
+ # class MyPhrase
19
+ # include PhraseIndexPolicy
20
+ #
21
+ # attr_accessor :key, :meta_key
22
+ #
23
+ # def initialize(key, meta_key)
24
+ # @key = key; @meta_key = meta_key
25
+ # end
26
+ # end
27
+ #
28
+ # p = MyPhrase.new('foo', 'bar')
29
+ # p.index_key # => :meta_key
30
+ # p.index_value # => 'bar'
31
+ #
32
+ # p = MyPhrase.new('foo', nil)
33
+ # p.index_key # => :key
34
+ # p.index_value # => 'foo'
35
+ #
36
+ # p = MyPhrase.new(nil, nil)
37
+ # p.index_key # => :key
38
+ # p.index_value # => ''
39
+ #
40
+ # @see Rosette::Core::Phrase
41
+ module PhraseIndexPolicy
42
+ # Determines which key should be used for indexing.
43
+ #
44
+ # @param [String] key The phrase key.
45
+ # @param [String] meta_key The phrase meta key.
46
+ # @return [Symbol] either +:key+ or +:meta_key+.
47
+ def self.index_key(key, meta_key)
48
+ if !meta_key || meta_key.empty?
49
+ :key
50
+ else
51
+ :meta_key
52
+ end
53
+ end
54
+
55
+ # Determines which value should be used for indexing.
56
+ #
57
+ # @param [String] key The phrase key.
58
+ # @param [String] meta_key The phrase meta_key.
59
+ # @return [String] either the given key or meta key, or an empty string
60
+ # if the value is +nil+. In other words, if the value at +#index_key+
61
+ # is +nil+, returns an empty string.
62
+ def self.index_value(key, meta_key)
63
+ value = case index_key(key, meta_key)
64
+ when :key then key
65
+ else meta_key
66
+ end
67
+
68
+ case value
69
+ when NilClass
70
+ ''
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ # Determines which key should be used for indexing.
77
+ #
78
+ # @return [Symbol] either +:key+ or +:meta_key+.
79
+ def index_key
80
+ PhraseIndexPolicy.index_key(key, meta_key)
81
+ end
82
+
83
+ # Determines which value should be used for indexing.
84
+ #
85
+ # @return [String] either the given key or meta key, or an empty string
86
+ # if the value is +nil+. In other words, if the value at +#index_key+
87
+ # is +nil+, returns an empty string.
88
+ def index_value
89
+ PhraseIndexPolicy.index_value(key, meta_key)
90
+ end
91
+
92
+ protected
93
+
94
+ def self.included(base)
95
+ base.class_eval do
96
+ def self.index_key(key, meta_key)
97
+ PhraseIndexPolicy.index_key(key, meta_key)
98
+ end
99
+
100
+ def self.index_value(key, meta_key)
101
+ PhraseIndexPolicy.index_value(key, meta_key)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+
3
+ module Rosette
4
+ module Core
5
+
6
+ # Turns a {Phrase} into a hash. Must be mixed into a {Phrase}-like class.
7
+ #
8
+ # @example
9
+ # p = Phrase.new
10
+ # p.key = 'foo'
11
+ # p.meta_key = 'bar'
12
+ # p.file = '/path/to/file.yml'
13
+ #
14
+ # p.to_h # => { key: 'foo', meta_key: 'bar', file: '/path/to/file.yml' ... }
15
+ module PhraseToHash
16
+ # Converts the attributes of a {Phrase} into a hash of attributes.
17
+ #
18
+ # @return [Hash] a hash of phrase attributes.
19
+ def to_h
20
+ {
21
+ key: key,
22
+ meta_key: meta_key,
23
+ file: file,
24
+ commit_id: commit_id,
25
+ author_name: author_name,
26
+ author_email: author_email,
27
+ line_number: line_number
28
+ }
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,339 @@
1
+ # encoding: UTF-8
2
+
3
+ module Rosette
4
+ module Core
5
+
6
+ # Configuration for a single repository. Instances of {RepoConfig} can
7
+ # be configured to:
8
+ #
9
+ # * Extract phrases (via {#add_extractor}). Phrase extraction means
10
+ # certain files (that you specify) will be monitored for changes and
11
+ # processed. For example, you could specify that all files with a .yml
12
+ # extension be monitored. When Rosette, using git, detects that any of
13
+ # those files have changed, it will parse the files using an extractor
14
+ # and store the phrases in the datastore. The Rosette project contains
15
+ # a number of pre-built extractors. Visit github for a complete list:
16
+ # https://github.com/rosette-proj. For example, the yaml extractor is
17
+ # called rosette-extractor-yaml and is available at
18
+ # https://github.com/rosette-proj/rosette-extractor-yaml. You would
19
+ # need to add it to your Gemfile and require it before use.
20
+ #
21
+ # * Serialize phrases (via {#add_serializer}). Serializing phrases can be
22
+ # thought of as the opposite of extracting them. Instead of parsing a
23
+ # yaml file for example, serialization is the process of turning a
24
+ # collection of foreign language translations into a big string of yaml
25
+ # that can be written to a file. Usually serialization happens when
26
+ # you're ready to export translations from Rosette. In the Rails world
27
+ # for example, you'd export (or serialize) translations per locale and
28
+ # store them as files in the config/locales directory. Spanish
29
+ # translations would be exported to config/locales/es.yml and Japanese
30
+ # translations to config/locales/ja.yml. The Rosette project contains
31
+ # a number of pre-built serializers. Visit github for a complete list:
32
+ # https://github.com/rosette-proj. For example, the yaml serializer is
33
+ # called rosette-serializer-yaml and is available at
34
+ # https://github.com/rosette-proj/rosette-serializer-yaml. You would
35
+ # need to add it to your Gemfile and require it before use.
36
+ #
37
+ # * Pre-process phrases using {SerializerConfig#add_preprocessor}.
38
+ # Serializers can also pre-process translations (see the example below).
39
+ # Pre-processing is the concept of modifying a translation just before
40
+ # it gets serialized. Examples include rosette-preprocessor-normalization,
41
+ # which is capable of applying Unicode's text normalization algorithm
42
+ # to translation text. See https://github.com/rosette-proj for a complete
43
+ # list of pre-processors.
44
+ #
45
+ # * Interact with third-party libraries or services via integrations (see
46
+ # the {#add_integration} method). Integrations are very general in that
47
+ # they can be almost anything. For the most part however, integrations
48
+ # serve as bridges to external APIs or libraries. For example, the
49
+ # Rosette project currently contains an integration called
50
+ # rosette-integration-smartling that's responsible for pushing and
51
+ # pulling translations to/from the Smartling translation platform
52
+ # (Smartling is a translation management system, or TMS). Since Rosette
53
+ # is not a TMS (i.e. doesn't provide any GUI for entering translations),
54
+ # you will need to use a third-party service like Smartling or build
55
+ # your own TMS solution. Another example is rosette-integration-rollbar.
56
+ # Rollbar is a third-party error reporting system. The Rollbar
57
+ # integration not only adds a Rosette-style {ErrorReporter}, it also
58
+ # hooks into a few places errors might happen, like Rosette::Server's
59
+ # rack stack.
60
+ #
61
+ # @example
62
+ # config = RepoConfig.new('my_repo')
63
+ # .set_path('/path/to/my_repo/.git')
64
+ # .set_description('My awesome repo')
65
+ # .set_source_locale('en-US')
66
+ # .add_locales(%w(pt-BR es-ES fr-FR ja-JP ko-KR))
67
+ # .add_extractor('yaml/rails') do |ext|
68
+ # ext.match_file_extension('.yml').and(
69
+ # ext.match_path('config/locales')
70
+ # )
71
+ # end
72
+ # .add_serializer('rails', format: 'yaml/rails') do |ser|
73
+ # ser.add_preprocessor('normalization') do |pre|
74
+ # pre.set_normalization_form(:nfc)
75
+ # end
76
+ # end
77
+ # .add_integration('smartling') do |sm|
78
+ # sm.set_api_options(smartling_api_key: 'fakefake', ... )
79
+ # sm.set_serializer('yaml/rails')
80
+ # end
81
+ #
82
+ # @!attribute [r] name
83
+ # @return [String] the name of the repository.
84
+ # @!attribute [r] repo
85
+ # @return [Repo] a {Repo} instance that can be used to perform git
86
+ # operations on the local working copy of the associated git
87
+ # repository.
88
+ # @!attribute [r] locales
89
+ # @return [Array<Locale>] a list of the locales this repo supports.
90
+ # @!attribute [r] hooks
91
+ # @return [Hash<Hash<Array<Proc>>>] a hash of callbacks. The outer hash
92
+ # contains the order while the inner hash contains the action. For
93
+ # example, if the +hooks+ hash has been configured to do something
94
+ # after commit, it might look like this:
95
+ # { after: { commit: [<Proc #0x238d3a>] } }
96
+ # @!attribute [r] description
97
+ # @return [String] a description of the repository.
98
+ # @!attribute [r] extractor_configs
99
+ # @return [Array<ExtractorConfig>] a list of the currently configured
100
+ # extractors.
101
+ # @!attribute [r] serializer_configs
102
+ # @return [Array<SerializerConfig>] a list of the currently configured
103
+ # serializers.
104
+ # @!attribute [r] tms
105
+ # @return [Rosette::Tms::Repository] a repository instance from the chosen
106
+ # translation management system.
107
+ class RepoConfig
108
+ include Integrations::Integratable
109
+
110
+ attr_reader :name, :rosette_config, :repo, :locales, :hooks, :description
111
+ attr_reader :extractor_configs, :serializer_configs, :tms
112
+ attr_reader :placeholder_regexes
113
+
114
+ # Creates a new repo config object.
115
+ #
116
+ # @param [String] name The name of the repository. Usually matches the
117
+ # name of the directory on disk, but that's not required.
118
+ def initialize(name, rosette_config)
119
+ @name = name
120
+ @rosette_config = rosette_config
121
+ @extractor_configs = []
122
+ @serializer_configs = []
123
+ @locales = []
124
+ @placeholder_regexes = []
125
+
126
+ @hooks = Hash.new do |h, key|
127
+ h[key] = Hash.new do |h2, key2|
128
+ h2[key2] = []
129
+ end
130
+ end
131
+ end
132
+
133
+ # Sets the path to the repository's .git directory.
134
+ #
135
+ # @param [String] path The path to the repository's .git directory.
136
+ # @return [void]
137
+ def set_path(path)
138
+ @repo = Repo.from_path(path)
139
+ end
140
+
141
+ # Sets the description of the repository. This is really just for
142
+ # annotation purposes, the description isn't used by Rosette.
143
+ #
144
+ # @param [String] desc The description text.
145
+ # @return [void]
146
+ def set_description(desc)
147
+ @description = desc
148
+ end
149
+
150
+ # Gets the path to the repository's .git directory.
151
+ #
152
+ # @return [String]
153
+ def path
154
+ repo.path if repo
155
+ end
156
+
157
+ # Gets the source locale (i.e. the locale all the source files are in).
158
+ # Defaults to en-US.
159
+ #
160
+ # @return [Locale] the source locale.
161
+ def source_locale
162
+ @source_locale ||= Locale.parse('en-US', Locale::DEFAULT_FORMAT)
163
+ end
164
+
165
+ # Sets the source locale.
166
+ #
167
+ # @param [String] code The locale code.
168
+ # @param [Symbol] format The format +locale+ is in.
169
+ # @return [void]
170
+ def set_source_locale(code, format = Locale::DEFAULT_FORMAT)
171
+ @source_locale = Locale.parse(code, format)
172
+ end
173
+
174
+ # Set the TMS (translation management system). TMSs must contain a class
175
+ # named +Repository+ that implements the [Rosette::Tms::Repository]
176
+ # interface.
177
+ #
178
+ # @param [Const, String] tms The TMS to use. When this parameter is a
179
+ # string, +use_tms+ will try to look up the corresponding +Tms+
180
+ # constant. If a constant is given instead, it's used without
181
+ # modifications. In both cases, the +Tms+ constant will have +configure+
182
+ # called on it and is expected to yield a configuration object.
183
+ # @param [Hash] options A hash of options passed to the TMS's
184
+ # constructor.
185
+ # @return [void]
186
+ def use_tms(tms, options = {})
187
+ const = case tms
188
+ when String
189
+ if const = find_tms_const(tms)
190
+ const
191
+ else
192
+ raise ArgumentError, "'#{tms}' couldn't be found."
193
+ end
194
+ when Class, Module
195
+ tms
196
+ else
197
+ raise ArgumentError, "'#{tms}' must be a String, Class, or Module."
198
+ end
199
+
200
+ @tms = const.configure(rosette_config, self) do |configurator|
201
+ yield configurator if block_given?
202
+ end
203
+
204
+ nil
205
+ end
206
+
207
+ # Adds an extractor to this repo.
208
+ #
209
+ # @param [String] extractor_id The id of the extractor you'd like to add.
210
+ # @yield [config] yields the extractor config
211
+ # @yieldparam config [ExtractorConfig]
212
+ # @return [void]
213
+ def add_extractor(extractor_id)
214
+ klass = ExtractorId.resolve(extractor_id)
215
+ extractor_configs << ExtractorConfig.new(extractor_id, klass).tap do |config|
216
+ yield config if block_given?
217
+ end
218
+ end
219
+
220
+ # Adds a serializer to this repo.
221
+ #
222
+ # @param [String] name A semantic name for this serializer. Means nothing
223
+ # to Rosette, simply a way for you to label the serializer.
224
+ # @param [Hash] options A hash of options containing the following entries:
225
+ # * +format+: The id of the serializer, eg. "yaml/rails".
226
+ # @yield [config] yields the serializer config
227
+ # @yieldparam config [SerializerConfig]
228
+ # @return [void]
229
+ def add_serializer(name, options = {})
230
+ serializer_id = options[:format]
231
+ klass = SerializerId.resolve(serializer_id)
232
+ serializer_configs << SerializerConfig.new(name, klass, serializer_id).tap do |config|
233
+ yield config if block_given?
234
+ end
235
+ end
236
+
237
+ # Adds a locale to the list of locales this repo supports.
238
+ #
239
+ # @param [String] locale_code The locale you'd like to add.
240
+ # @param [Symbol] format The format of +locale_code+.
241
+ # @return [void]
242
+ def add_locale(locale_code, format = Locale::DEFAULT_FORMAT)
243
+ add_locales(locale_code)
244
+ end
245
+
246
+ # Adds multiple locales to the list of locales this repo supports.
247
+ #
248
+ # @param [Array<String>] locale_codes The list of locales to add.
249
+ # @param [Symbol] format The format of +locale_codes+.
250
+ # @return [void]
251
+ def add_locales(locale_codes, format = Locale::DEFAULT_FORMAT)
252
+ @locales += Array(locale_codes).map do |locale_code|
253
+ Locale.parse(locale_code, format)
254
+ end
255
+ end
256
+
257
+ # Adds an after hook. You should pass a block to this method. The
258
+ # block will be executed when the hook fires.
259
+ #
260
+ # @param [Symbol] action The action to hook. Currently the only
261
+ # supported action is +:commit+.
262
+ # @return [void]
263
+ def after(action, &block)
264
+ hooks[:after][action] << block
265
+ end
266
+
267
+ # Retrieves the extractor configs that match the given path.
268
+ #
269
+ # @param [String] path The path to match.
270
+ # @return [Array<ExtractorConfig>] a list of the extractor configs that
271
+ # were found to match +path+.
272
+ def get_extractor_configs(path)
273
+ extractor_configs.select do |config|
274
+ config.matches?(path)
275
+ end
276
+ end
277
+
278
+ # Retrieves the extractor config by either name or extractor id.
279
+ #
280
+ # @param [String] name_or_id The name or extractor id.
281
+ # @return [nil, ExtractorConfig] the first matching extractor config.
282
+ # Potentially returns +nil+ if no matching extractor config can be
283
+ # found.
284
+ def get_extractor_config(extractor_id)
285
+ extractor_configs.find do |config|
286
+ config.extractor_id == extractor_id
287
+ end
288
+ end
289
+
290
+ # Retrieves the serializer config by either name or serializer id.
291
+ #
292
+ # @param [String] name_or_id The name or serializer id.
293
+ # @return [nil, SerializerConfig] the first matching serializer config.
294
+ # Potentially returns +nil+ if no matching serializer config can be
295
+ # found.
296
+ def get_serializer_config(name_or_id)
297
+ found = serializer_configs.find do |config|
298
+ config.name == name_or_id
299
+ end
300
+
301
+ found || serializer_configs.find do |config|
302
+ config.serializer_id == name_or_id
303
+ end
304
+ end
305
+
306
+ # Retrieves the locale object by locale code.
307
+ #
308
+ # @param [String] code The locale code to look for.
309
+ # @param [Symbol] format The locale format +code+ is in.
310
+ # @return [nil, Locale] The locale who's code matches +code+. Potentially
311
+ # returns +nil+ if the locale can't be found.
312
+ def get_locale(code, format = Locale::DEFAULT_FORMAT)
313
+ locale_to_find = Locale.parse(code, format)
314
+ locales.find { |locale| locale == locale_to_find }
315
+ end
316
+
317
+ # Adds a regex that matches a placeholder in translation text. For
318
+ # example, Ruby placeholders often look like this "Hello %{name}!".
319
+ # Some integrations rely on these regexes to detect and format
320
+ # placeholders correctly.
321
+ #
322
+ # @param [Regexp] placeholder_regex The regex to add.
323
+ # @return [void]
324
+ def add_placeholder_regex(placeholder_regex)
325
+ placeholder_regexes << placeholder_regex
326
+ end
327
+
328
+ protected
329
+
330
+ def find_tms_const(name)
331
+ const_str = "#{Rosette::Core::StringUtils.camelize(name)}Tms"
332
+
333
+ if Rosette::Tms.const_defined?(const_str)
334
+ Rosette::Tms.const_get(const_str)
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end