slang 0.34.0

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.
@@ -0,0 +1,139 @@
1
+ require "bundler/setup"
2
+
3
+ require "logger"
4
+ require "set"
5
+ require "time"
6
+ require "uri"
7
+ require "zlib"
8
+
9
+ require "oj"
10
+
11
+ require "slang/snapshot"
12
+ require "slang/snapshot/locale"
13
+ require "slang/snapshot/template"
14
+ require "slang/snapshot/translation"
15
+
16
+ require "slang/updater/development"
17
+ require "slang/updater/key_reporter"
18
+ require "slang/updater/production"
19
+ require "slang/updater/shared_state"
20
+
21
+ require "slang/version"
22
+
23
+ module Slang
24
+ USER_AGENT = "slang-rb/#{VERSION}"
25
+ KEY_REGEX = /\A[0-9a-zA-Z_\-]{20}\z/
26
+
27
+ class ConfigurationError < SlangError; end
28
+ class SnapshotError < SlangError; end
29
+ class NetworkError < SlangError
30
+ attr_reader :retry_in
31
+ def initialize(retry_in=nil)
32
+ @retry_in = retry_in
33
+ end
34
+ end
35
+
36
+ def self.internal_translate(key, variable_hash)
37
+ snapshot.translate(locale_code, key, variable_hash)
38
+ end
39
+
40
+ def self.snapshot
41
+ raise SlangError, "not initialized." unless @initialized
42
+ @updater.snapshot
43
+ end
44
+
45
+ def self.key_reporter
46
+ @key_reporter
47
+ end
48
+
49
+ def self.env
50
+ @env
51
+ end
52
+
53
+ def self.log_error(message)
54
+ if @logger
55
+ @logger.error("[SLANG] #{message}")
56
+ elsif !defined?(Slang::TEST)
57
+ warn(message)
58
+ end
59
+ end
60
+
61
+ def self.log_warn(message)
62
+ if @logger
63
+ @logger.warn("[SLANG] #{message}")
64
+ elsif !defined?(Slang::TEST)
65
+ warn(message)
66
+ end
67
+ end
68
+
69
+ def self.log_info(message)
70
+ if @logger
71
+ @logger.info("[SLANG] #{message}")
72
+ end
73
+ end
74
+
75
+ def self.log_debug(message)
76
+ if @logger
77
+ @logger.debug("[SLANG] #{message}")
78
+ end
79
+ end
80
+
81
+ def self.env_var(var)
82
+ value = ENV[var]
83
+ value if value && !value.empty?
84
+ end
85
+
86
+ def self.init(config={})
87
+ return false if @initialized
88
+
89
+ api_base = config[:api_base] ? config[:api_base].chomp("/") : nil
90
+ data_dir = config[:data_dir] || env_var("SLANG_DATA_DIR") || File.join(defined?(Rails) ? Rails.root : Dir.pwd, "slang_data")
91
+ developer_key = config[:developer_key] || env_var("SLANG_DEVELOPER_KEY")
92
+ embedded_snapshot = config[:embedded_snapshot] || env_var("SLANG_EMBEDDED_SNAPSHOT")
93
+ env = config[:env] || env_var("SLANG_ENV") || (defined?(Rails) ? Rails.env : nil) || (config[:developer_key] ? "development" : "production")
94
+ project_key = config[:project_key] || env_var("SLANG_PROJECT_KEY")
95
+
96
+ @env = env
97
+ dev_mode = @env =~ /\Adev/i
98
+ @logger ||= defined?(Rails) ? Rails.logger : nil
99
+
100
+ Slang.log_info("Slang v#{Slang::VERSION} starting in #{dev_mode ? 'development' : 'production'} mode (#{env} environment)")
101
+
102
+ begin
103
+ FileUtils.mkdir_p(data_dir, mode: 0755)
104
+ rescue
105
+ raise ConfigurationError, "unable to create data dir - #{data_dir}"
106
+ end
107
+ raise ConfigurationError, "missing/malformed project key (project_key=#{project_key})" unless project_key =~ KEY_REGEX
108
+
109
+ if dev_mode
110
+ raise ConfigurationError, "missing/malformed developer key (developer_key=#{developer_key})" unless developer_key =~ KEY_REGEX
111
+ api_base ||= "https://dev.getslang.com"
112
+ @updater = Updater::Development.new(env, "#{api_base}/snapshot/#{project_key}", data_dir, embedded_snapshot)
113
+ @updater.dev_key = developer_key
114
+ @key_reporter = Updater::KeyReporter.new("#{api_base}/keys/#{project_key}", developer_key)
115
+ else
116
+ api_base ||= "https://slang-a.akamaihd.net"
117
+ @updater = Updater::Production.new(env, "#{api_base}/m/#{project_key}", data_dir, embedded_snapshot)
118
+ end
119
+
120
+ Slang.log_info("Slang initialized. All systems go!")
121
+ @initialized = true
122
+
123
+ if puma_preload?
124
+ Puma.cli_config.options[:before_worker_boot] << Proc.new { Slang.snapshot } # delay to worker start
125
+ # TODO: unicorn check
126
+ else
127
+ Slang.snapshot # trigger background fetch immediately
128
+ end
129
+ nil
130
+ end
131
+
132
+ private
133
+
134
+ def self.puma_preload?
135
+ defined?(Puma) && defined?(Puma.cli_config) && Puma.cli_config && Puma.cli_config.options[:preload_app] && (Puma.cli_config.options[:workers] > 0)
136
+ end
137
+
138
+ end
139
+ require "slang/railtie" if defined?(Rails)
@@ -0,0 +1,16 @@
1
+ module Slang
2
+
3
+ class Railtie < Rails::Railtie
4
+
5
+ initializer "slang" do
6
+ ActiveSupport.on_load(:action_view) do
7
+ include Slang::Helpers
8
+ end
9
+ ActiveSupport.on_load(:action_controller) do
10
+ include Slang::Helpers
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,131 @@
1
+ require "slang/snapshot/warnings"
2
+ module Slang
3
+
4
+ # A snapshot is a point-in-time state of all known keys and their associated translations.
5
+ #
6
+ class Snapshot
7
+ include Warnings
8
+
9
+ attr_reader :id # String
10
+ attr_reader :timestamp # Integer unix-time
11
+ attr_reader :default_locale # Locale
12
+ attr_reader :locales # Hash of :locale_code => Locale
13
+
14
+ EMPTY_SNAPSHOT_JSON = '[6,"00000000000000000000",0,"en",[["en",null,"one_other",null]],[]]'
15
+
16
+ # Create a snapshot from a serialized JSON string.
17
+ #
18
+ def self.from_json(json)
19
+ Snapshot.new(array_from_json(json))
20
+ end
21
+
22
+ # Create a snapshot array from a serialized JSON string.
23
+ #
24
+ def self.array_from_json(json)
25
+ Oj.strict_load(json.force_encoding(Encoding::UTF_8))
26
+ end
27
+
28
+ # Create a Snapshot object.
29
+ #
30
+ # @param [Array] deserialized snapshot array
31
+ # @raise [SnapshotError] if a corrupt snapshot is detected
32
+ #
33
+ def initialize(array)
34
+ snapshot_format, @id, @timestamp, default_locale_code, locales_array, translations_array = *array.to_a
35
+ @locales = {}
36
+ locales_array.each_with_index do |locale_array, i|
37
+ locale = Locale.new(locale_array, i)
38
+ @locales[locale.code] = locale
39
+ locale.aliases.each { |a| @locales[a] = locale }
40
+ end
41
+ @default_locale = @locales[default_locale_code.to_sym] || @locales.first[1]
42
+ raise SnapshotError, "default locale does not exist" unless @default_locale
43
+
44
+ @translations = {}
45
+ translations_array.each do |translation_array|
46
+ t = Translation.new(translation_array)
47
+ @translations[t.key] = t if t.rules
48
+ end
49
+ super() # invoke Warnings#initialize
50
+ end
51
+
52
+
53
+ # Fetch all the translation keys.
54
+ #
55
+ # @return [Array<String>] known keys.
56
+ #
57
+ def keys
58
+ @translations.keys
59
+ end
60
+
61
+ # Perform a translation for the given key/locale, with an optional variable map.
62
+ #
63
+ # @param [Symbol] locale code
64
+ # @param [#to_s] key
65
+ # @param [Hash] variable hash
66
+ #
67
+ # @return [String] always returns a String (typically a translation)
68
+ #
69
+ def translate(locale_code, key, vm)
70
+ key = key.to_s.downcase
71
+ variable_map = {}
72
+ vm.each { |k, v| variable_map[k.to_s.downcase.freeze] = v }
73
+ if Slang.key_reporter
74
+ raise ArgumentError, "Slang key name malformed: #{key}" unless key =~ Translation::KEY_REGEX
75
+ variable_map.each do |k, v|
76
+ raise ArgumentError, "Slang variable name malformed: #{k}" unless k =~ Template::VARIABLE_NAME_REGEX
77
+ end
78
+ end
79
+ internal_translate(locale_code, key, variable_map)
80
+ end
81
+
82
+ private
83
+
84
+ # Internal translate method.
85
+ #
86
+ # @param [Symbol] locale code
87
+ # @param [String] downcased string key
88
+ # @param [Hash{String => #to_s}] variable map with all keys as downcased strings
89
+ #
90
+ # @return [String] translation
91
+ #
92
+ def internal_translate(locale_code, key, variable_map)
93
+ locale = @locales[locale_code] || @default_locale
94
+
95
+ # lookup translation
96
+ translation = @translations[key]
97
+ result, string, missing_names, selector = translation.evaluate(locale, variable_map) if translation
98
+ case result
99
+ when :success
100
+ # all is good
101
+ when :missing_interpolation_variables
102
+ missing_interpolation_variables!(locale, key, selector, missing_names, variable_map)
103
+ when :missing_rule_variables
104
+ missing_rule_variables!(locale, key, missing_names, variable_map)
105
+ when :missing_rule_templates
106
+ missing_rule_templates!(locale, key, missing_names, variable_map)
107
+ else # :missing_template or nil
108
+ missing_key!(locale, key, variable_map)
109
+ fallback = internal_translate(locale.fallback, key, variable_map) if locale.fallback
110
+ return fallback if fallback
111
+ if Slang.key_reporter
112
+ Slang.log_info("reporting unknown key.. #{key}")
113
+ Slang.key_reporter.unknown_key(key, variable_map)
114
+ # development string placeholder
115
+ var_names = variable_map.keys.sort!
116
+ string = key.split(".").map! do |component|
117
+ component.split("_").map! { |c| c.capitalize }.join
118
+ end.join(" ")
119
+ unless var_names.empty?
120
+ string << " (" << var_names.map { |n| "#{n}=#{variable_map[n]}" }.join(", ") << ")"
121
+ end
122
+ else
123
+ # production string placeholder
124
+ string="{{#{locale.code}:#{key}}}"
125
+ end
126
+ end
127
+ string
128
+ end
129
+
130
+ end # class
131
+ end
@@ -0,0 +1,35 @@
1
+ module Slang
2
+ class Snapshot
3
+
4
+ # A Locale is an immutable object that holds a locale code (and optional aliases), pluralization rule, and fallback
5
+ # locale.
6
+ #
7
+ class Locale
8
+
9
+ attr_reader :code
10
+ attr_reader :fallback
11
+ attr_reader :aliases
12
+ attr_reader :pluralization_method
13
+ attr_reader :translation_index
14
+
15
+ # Create a locale object from the snapshot locale array.
16
+ #
17
+ # @param [Array] snapshot locale array
18
+ # @param [Integer] index of this locale's translation in translation array
19
+ #
20
+ def initialize(locale_array, translation_index)
21
+ code, fallback, pluralization, _, *aliases = *locale_array
22
+ @code = code.to_sym
23
+ @fallback = fallback.to_sym if fallback
24
+ @pluralization_method = "pluralization_#{pluralization}".to_sym
25
+ unless Rules.method_defined?(@pluralization_method)
26
+ Slang.log_warn("Ignoring unknown pluralization rule '#{pluralization}' in locale '#{code}'.")
27
+ @pluralization_method = :pluralization_unknown
28
+ end
29
+ @aliases = aliases.map(&:to_sym)
30
+ @translation_index = translation_index
31
+ end
32
+
33
+ end # class
34
+ end
35
+ end
@@ -0,0 +1,239 @@
1
+ module Slang
2
+ class Snapshot
3
+
4
+ # Rule-logic, mixed into the Translation class.
5
+ #
6
+ module Rules
7
+
8
+ RULE_TYPES = {
9
+ "N" => :pluralization,
10
+ "G" => :gender
11
+ }
12
+
13
+ # Generate rules array from a rule pattern.
14
+ #
15
+ # @param [String] rule pattern of the form "var1=N" or "var1=N:var2=G:..."
16
+ #
17
+ # @return [Array<Array<(String,Symbol)>>,nil] rules array, nil on error
18
+ #
19
+ def rules_from_pattern(rule_pattern)
20
+ rule_pattern.to_s.split(":").map! do |rule|
21
+ variable_name, rule_code = rule.split("=")
22
+ rule_type = RULE_TYPES[rule_code]
23
+ return nil unless rule_type
24
+ [ variable_name, rule_type ]
25
+ end
26
+ end
27
+
28
+ # Invokes the appropriate rule with the given value. This returns one or more template selectors (which may be
29
+ # combined with other rules) that will specify the appropriate template.
30
+ #
31
+ # @param [Symbol] rule-type
32
+ # @param [Object] rule argument
33
+ #
34
+ # @return [Array<String>] template selectors to try, in order
35
+ #
36
+ def evaluate_rule(locale, rule_type, value)
37
+ case rule_type
38
+ when :pluralization
39
+ send(locale.pluralization_method, value)
40
+ when :gender
41
+ gender(genderize(value))
42
+ end
43
+ end
44
+
45
+ ### Gender rule
46
+
47
+ GENDER_FEMALE = %w(female)
48
+ GENDER_MALE = %w(male)
49
+ GENDER_UNKNOWN = %w(unknown)
50
+
51
+ # Determine gender rule based on given value.
52
+ #
53
+ # @param [Symbol] gender value should be :female, :male, or :unknown
54
+ #
55
+ # @return [Array(String)] an 1-element array of the gender.
56
+ #
57
+ def gender(value)
58
+ case value
59
+ when :female; GENDER_FEMALE
60
+ when :male; GENDER_MALE
61
+ else GENDER_UNKNOWN
62
+ end
63
+ end
64
+
65
+ # Attempts to convert value into :female, :male, or :unknown. Any value
66
+ # whose string form starts with "f" becomes :female, "m" becomes :male,
67
+ # otherwise :unknown.
68
+ #
69
+ # @return [Symbol] :female, :male, or :unknown
70
+ #
71
+ def genderize(value)
72
+ if (value == :female) || (value == :male)
73
+ value
74
+ elsif (value == :unknown) || value.nil?
75
+ :unknown
76
+ else
77
+ case value.to_s
78
+ when /\Af/i; :female
79
+ when /\Am/i; :male
80
+ else :unknown
81
+ end
82
+ end
83
+ end
84
+
85
+ ### Pluralization rules
86
+
87
+ PLURAL_FEW = %w(few)
88
+ PLURAL_MANY = %w(many)
89
+ PLURAL_ONE = %w(one)
90
+ PLURAL_OTHER = %w(other)
91
+ PLURAL_TWO = %w(two)
92
+ PLURAL_ZERO = %w(zero)
93
+ PLURAL_ZERO_OR_ONE = %w(zero one)
94
+ PLURAL_ZERO_OR_MANY = %w(zero many)
95
+ PLURAL_ZERO_OR_OTHER = %w(zero other)
96
+
97
+ def pluralization_unknown(n)
98
+ PLURAL_OTHER
99
+ end
100
+
101
+ # Pluralization rule for languages with non-countable nouns "other",
102
+ # optionally supports the "zero" case.
103
+ #
104
+ # Example languages: Chinese, Japanese, Korean.
105
+ #
106
+ # @param [Number] integer or fraction
107
+ #
108
+ # @return [Array<String>] template selector(s)
109
+ #
110
+ def pluralization_other(n)
111
+ if n == 0
112
+ PLURAL_ZERO_OR_OTHER
113
+ else
114
+ PLURAL_OTHER
115
+ end
116
+ end
117
+
118
+ # Pluralization rule for languages with "one" or "other", optionally
119
+ # supports "zero" case.
120
+ #
121
+ # Example languages: English, German, Italian, Portugeuse, Spanish.
122
+ #
123
+ # @param [Number] integer or fraction
124
+ #
125
+ # @return [Array<String>] template selector(s)
126
+ #
127
+ def pluralization_one_other(n)
128
+ if n == 0
129
+ PLURAL_ZERO_OR_OTHER
130
+ elsif n == 1
131
+ PLURAL_ONE
132
+ else
133
+ PLURAL_OTHER
134
+ end
135
+ end
136
+
137
+ # Pluralization rule for languages with "one" for zero/one, or "other",
138
+ # optionally supports "zero" case.
139
+ #
140
+ # Example languages: French
141
+ #
142
+ # @param [Number] integer or fraction
143
+ #
144
+ # @return [Array<String>] template selector(s)
145
+ #
146
+ def pluralization_zero_and_one_other(n)
147
+ if n == 0
148
+ PLURAL_ZERO_OR_ONE
149
+ elsif n == 1
150
+ PLURAL_ONE
151
+ else
152
+ PLURAL_OTHER
153
+ end
154
+ end
155
+
156
+ # Pluralization rule for east slavic languages with "one", "few", "many",
157
+ # "other", optionally supports "zero" case.
158
+ #
159
+ # Example languages: RUssian
160
+ #
161
+ # @param [Number] integer or fraction
162
+ #
163
+ # @return [Array<String>] template selector(s)
164
+ #
165
+ def pluralization_east_slavic(n)
166
+ if n == 0
167
+ PLURAL_ZERO_OR_MANY
168
+ else
169
+ mod10 = n % 10
170
+ mod100 = n % 100
171
+ if (mod10 == 1) && (mod100 != 11)
172
+ PLURAL_ONE
173
+ elsif ((mod10 >= 2) && (mod10 <= 4)) && !((mod100 >= 12) && (mod100 <= 14))
174
+ PLURAL_FEW
175
+ elsif (mod10 == 0) || ((mod10 >= 5) && (mod10 <= 9)) || ((mod100 >= 11) && (mod100 <= 14))
176
+ PLURAL_MANY
177
+ else
178
+ PLURAL_OTHER
179
+ end
180
+ end
181
+ end
182
+
183
+ # Pluralization rule for Arabic with "zero", "one", "two", "few", "many",
184
+ # and "other". Note "zero" case is required.
185
+ #
186
+ # Example languages: Arabic.
187
+ #
188
+ # @param [Number] integer or fraction
189
+ #
190
+ # @return [Array<String>] template selector(s)
191
+ #
192
+ def pluralization_arabic(n)
193
+ if n == 0
194
+ PLURAL_ZERO
195
+ elsif n == 1
196
+ PLURAL_ONE
197
+ elsif n == 2
198
+ PLURAL_TWO
199
+ else
200
+ mod100 = n % 100
201
+ if (mod100 >= 3) && (mod100 <= 10)
202
+ PLURAL_FEW
203
+ elsif (mod100 >= 11)
204
+ PLURAL_MANY
205
+ else
206
+ PLURAL_OTHER
207
+ end
208
+ end
209
+ end
210
+
211
+ # Pluralization rule for Polish with "one", "few", "many", and "other", optionally supports "zero" case.
212
+ #
213
+ # @param [Number] integer or fraction
214
+ #
215
+ # @return [Array<String>] template selector(s)
216
+ #
217
+ def pluralization_polish(n)
218
+ if Float === n
219
+ PLURAL_OTHER
220
+ elsif n == 0
221
+ PLURAL_ZERO_OR_MANY
222
+ elsif n == 1
223
+ PLURAL_ONE
224
+ else
225
+ mod10 = n % 10
226
+ mod100 = n % 100
227
+ if ((mod10 >= 2) && (mod10 <= 4)) && !((mod100 >= 12) && (mod100 <= 14))
228
+ PLURAL_FEW
229
+ elsif ((mod10 == 0) || (mod10 == 1)) || ((mod10 >= 5) && (mod10 <= 9)) || ((mod100 >= 12) && (mod100 <= 14))
230
+ PLURAL_MANY
231
+ else
232
+ PLURAL_OTHER
233
+ end
234
+ end
235
+ end
236
+
237
+ end # module
238
+ end
239
+ end