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.
- checksums.yaml +7 -0
- data/Gemfile +26 -0
- data/History.txt +3 -0
- data/README.md +94 -0
- data/Rakefile +18 -0
- data/lib/rosette/core.rb +110 -0
- data/lib/rosette/core/branch_utils.rb +152 -0
- data/lib/rosette/core/commands.rb +139 -0
- data/lib/rosette/core/commands/errors.rb +17 -0
- data/lib/rosette/core/commands/git/commit_command.rb +65 -0
- data/lib/rosette/core/commands/git/diff_base_command.rb +301 -0
- data/lib/rosette/core/commands/git/diff_command.rb +188 -0
- data/lib/rosette/core/commands/git/diff_entry.rb +44 -0
- data/lib/rosette/core/commands/git/fetch_command.rb +27 -0
- data/lib/rosette/core/commands/git/repo_snapshot_command.rb +40 -0
- data/lib/rosette/core/commands/git/show_command.rb +70 -0
- data/lib/rosette/core/commands/git/snapshot_command.rb +50 -0
- data/lib/rosette/core/commands/git/status_command.rb +128 -0
- data/lib/rosette/core/commands/git/with_non_merge_ref.rb +48 -0
- data/lib/rosette/core/commands/git/with_ref.rb +92 -0
- data/lib/rosette/core/commands/git/with_refs.rb +92 -0
- data/lib/rosette/core/commands/git/with_repo_name.rb +50 -0
- data/lib/rosette/core/commands/git/with_snapshots.rb +45 -0
- data/lib/rosette/core/commands/queuing/enqueue_commit_command.rb +37 -0
- data/lib/rosette/core/commands/queuing/requeue_commit_command.rb +46 -0
- data/lib/rosette/core/commands/translations/export_command.rb +257 -0
- data/lib/rosette/core/commands/translations/translation_lookup_command.rb +66 -0
- data/lib/rosette/core/commands/translations/with_locale.rb +47 -0
- data/lib/rosette/core/configurator.rb +160 -0
- data/lib/rosette/core/error_reporters/buffered_error_reporter.rb +96 -0
- data/lib/rosette/core/error_reporters/error_reporter.rb +31 -0
- data/lib/rosette/core/error_reporters/nil_error_reporter.rb +25 -0
- data/lib/rosette/core/error_reporters/printing_error_reporter.rb +58 -0
- data/lib/rosette/core/error_reporters/raising_error_reporter.rb +27 -0
- data/lib/rosette/core/errors.rb +93 -0
- data/lib/rosette/core/extractor/commit_log.rb +33 -0
- data/lib/rosette/core/extractor/commit_log_status.rb +57 -0
- data/lib/rosette/core/extractor/commit_processor.rb +109 -0
- data/lib/rosette/core/extractor/extractor.rb +72 -0
- data/lib/rosette/core/extractor/extractor_config.rb +74 -0
- data/lib/rosette/core/extractor/locale.rb +118 -0
- data/lib/rosette/core/extractor/phrase.rb +76 -0
- data/lib/rosette/core/extractor/phrase/phrase_index_policy.rb +108 -0
- data/lib/rosette/core/extractor/phrase/phrase_to_hash.rb +33 -0
- data/lib/rosette/core/extractor/repo_config.rb +339 -0
- data/lib/rosette/core/extractor/serializer_config.rb +55 -0
- data/lib/rosette/core/extractor/static_extractor.rb +44 -0
- data/lib/rosette/core/extractor/translation.rb +44 -0
- data/lib/rosette/core/extractor/translation/translation_to_hash.rb +28 -0
- data/lib/rosette/core/git/diff_finder.rb +131 -0
- data/lib/rosette/core/git/ref.rb +116 -0
- data/lib/rosette/core/git/repo.rb +378 -0
- data/lib/rosette/core/path_matcher_factory.rb +330 -0
- data/lib/rosette/core/resolvers/extractor_id.rb +37 -0
- data/lib/rosette/core/resolvers/integration_id.rb +37 -0
- data/lib/rosette/core/resolvers/preprocessor_id.rb +38 -0
- data/lib/rosette/core/resolvers/resolver.rb +115 -0
- data/lib/rosette/core/resolvers/serializer_id.rb +37 -0
- data/lib/rosette/core/snapshots/cached_head_snapshot_factory.rb +51 -0
- data/lib/rosette/core/snapshots/cached_snapshot_factory.rb +67 -0
- data/lib/rosette/core/snapshots/head_snapshot_factory.rb +58 -0
- data/lib/rosette/core/snapshots/repo_config_path_filter.rb +83 -0
- data/lib/rosette/core/snapshots/snapshot_factory.rb +184 -0
- data/lib/rosette/core/string_utils.rb +23 -0
- data/lib/rosette/core/translation_status.rb +81 -0
- data/lib/rosette/core/validators.rb +18 -0
- data/lib/rosette/core/validators/commit_validator.rb +62 -0
- data/lib/rosette/core/validators/commits_validator.rb +32 -0
- data/lib/rosette/core/validators/encoding_validator.rb +32 -0
- data/lib/rosette/core/validators/locale_validator.rb +37 -0
- data/lib/rosette/core/validators/repo_validator.rb +33 -0
- data/lib/rosette/core/validators/serializer_validator.rb +37 -0
- data/lib/rosette/core/validators/validator.rb +31 -0
- data/lib/rosette/core/version.rb +8 -0
- data/lib/rosette/data_stores.rb +11 -0
- data/lib/rosette/data_stores/errors.rb +26 -0
- data/lib/rosette/data_stores/phrase_status.rb +59 -0
- data/lib/rosette/integrations.rb +12 -0
- data/lib/rosette/integrations/errors.rb +15 -0
- data/lib/rosette/integrations/integratable.rb +58 -0
- data/lib/rosette/integrations/integration.rb +23 -0
- data/lib/rosette/preprocessors.rb +11 -0
- data/lib/rosette/preprocessors/errors.rb +14 -0
- data/lib/rosette/preprocessors/preprocessor.rb +48 -0
- data/lib/rosette/queuing.rb +14 -0
- data/lib/rosette/queuing/commits.rb +19 -0
- data/lib/rosette/queuing/commits/commit_conductor.rb +90 -0
- data/lib/rosette/queuing/commits/commit_job.rb +93 -0
- data/lib/rosette/queuing/commits/commits_queue_configurator.rb +60 -0
- data/lib/rosette/queuing/commits/extract_stage.rb +46 -0
- data/lib/rosette/queuing/commits/fetch_stage.rb +51 -0
- data/lib/rosette/queuing/commits/finalize_stage.rb +76 -0
- data/lib/rosette/queuing/commits/phrase_storage_granularity.rb +20 -0
- data/lib/rosette/queuing/commits/push_stage.rb +91 -0
- data/lib/rosette/queuing/commits/stage.rb +96 -0
- data/lib/rosette/queuing/job.rb +74 -0
- data/lib/rosette/queuing/queue.rb +28 -0
- data/lib/rosette/queuing/queue_configurator.rb +76 -0
- data/lib/rosette/queuing/worker.rb +30 -0
- data/lib/rosette/serializers.rb +10 -0
- data/lib/rosette/serializers/serializer.rb +98 -0
- data/lib/rosette/tms.rb +9 -0
- data/lib/rosette/tms/repository.rb +95 -0
- data/rosette-core.gemspec +24 -0
- data/spec/core/branch_utils_spec.rb +110 -0
- data/spec/core/commands/git/commit_command_spec.rb +60 -0
- data/spec/core/commands/git/diff_command_spec.rb +263 -0
- data/spec/core/commands/git/fetch_command_spec.rb +61 -0
- data/spec/core/commands/git/repo_snapshot_command_spec.rb +72 -0
- data/spec/core/commands/git/show_command_spec.rb +128 -0
- data/spec/core/commands/git/snapshot_command_spec.rb +86 -0
- data/spec/core/commands/git/status_command_spec.rb +154 -0
- data/spec/core/commands/queuing/enqueue_commit_command_spec.rb +34 -0
- data/spec/core/commands/queuing/requeue_commit_command_spec.rb +46 -0
- data/spec/core/commands/translations/export_command_spec.rb +113 -0
- data/spec/core/commands/translations/translation_lookup_command_spec.rb +58 -0
- data/spec/core/configurator_spec.rb +47 -0
- data/spec/core/error_reporters/buffered_error_reporter_spec.rb +61 -0
- data/spec/core/error_reporters/nil_error_reporter_spec.rb +16 -0
- data/spec/core/error_reporters/printing_error_reporter_spec.rb +60 -0
- data/spec/core/extractor/commit_log_status_spec.rb +216 -0
- data/spec/core/extractor/commit_processor_spec.rb +68 -0
- data/spec/core/extractor/extractor_config_spec.rb +47 -0
- data/spec/core/extractor/extractor_spec.rb +26 -0
- data/spec/core/extractor/locale_spec.rb +92 -0
- data/spec/core/extractor/phrase/phrase_index_policy_spec.rb +116 -0
- data/spec/core/extractor/phrase/phrase_to_hash_spec.rb +18 -0
- data/spec/core/extractor/repo_config_spec.rb +147 -0
- data/spec/core/extractor/translation/translation_to_hash_spec.rb +25 -0
- data/spec/core/git/diff_finder_spec.rb +74 -0
- data/spec/core/git/ref_spec.rb +118 -0
- data/spec/core/git/repo_spec.rb +216 -0
- data/spec/core/path_matcher_factory_spec.rb +139 -0
- data/spec/core/resolvers/extractor_id_spec.rb +47 -0
- data/spec/core/resolvers/integration_id_spec.rb +47 -0
- data/spec/core/resolvers/preprocessor_id_spec.rb +47 -0
- data/spec/core/resolvers/serializer_id_spec.rb +47 -0
- data/spec/core/snapshots/snapshot_factory_spec.rb +145 -0
- data/spec/core/string_utils_spec.rb +19 -0
- data/spec/core/translation_status_spec.rb +91 -0
- data/spec/core/validators/commit_validator_spec.rb +40 -0
- data/spec/core/validators/encoding_validator_spec.rb +30 -0
- data/spec/core/validators/locale_validator_spec.rb +31 -0
- data/spec/core/validators/repo_validator_spec.rb +30 -0
- data/spec/core/validators/serializer_validator_spec.rb +31 -0
- data/spec/integrations/integratable_spec.rb +58 -0
- data/spec/queuing/commits/commit_conductor_spec.rb +71 -0
- data/spec/queuing/commits/commit_job_spec.rb +87 -0
- data/spec/queuing/commits/extract_stage_spec.rb +68 -0
- data/spec/queuing/commits/fetch_stage_spec.rb +101 -0
- data/spec/queuing/commits/finalize_stage_spec.rb +88 -0
- data/spec/queuing/commits/push_stage_spec.rb +145 -0
- data/spec/queuing/commits/stage_spec.rb +80 -0
- data/spec/queuing/job_spec.rb +33 -0
- data/spec/queuing/queue_configurator_spec.rb +44 -0
- data/spec/spec_helper.rb +90 -0
- data/spec/test_helpers/fake_commit_stage.rb +17 -0
- 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
|