rosette-core 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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