ghost_reader 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
6
+ *~
7
+ *.swp
8
+ \#*
9
+ .#*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.8.7@ghost_reader
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ghost_reader.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ GhostReader
2
+ ===========
3
+
4
+ i18n backend to ghost_writer service
5
+
6
+ ## Usage
7
+
8
+ add a folowing to `config/initializers/ghost_reader.rb`
9
+
10
+ I18n.backend=GhostReader::Backend.new("HTTP_URL_TO_GHOST_SERVER",
11
+ :default_backend=>I18n.backend, :wait_time=>30,
12
+ :trace => Proc.new do |message|
13
+ Rails.logger.debug message
14
+ end)
15
+
16
+ ### wait_time
17
+ The 'wait_time' is the minimum time in seconds after which reached a change
18
+ from the Ghost_Writer Ghost_Client. A low value minimizes the delay,
19
+ a high value minimizes the network-traffic.
20
+ Default-Value is 30
21
+
22
+ ### default_backend
23
+ The Ghost_reader tries to find default-values for not found translations and
24
+ posts them to the server together with the statistical data.
25
+
26
+ ### max_packet_size
27
+ For preventing Errors on receiving Server splits the ghost-reader the posts
28
+ in parts with max max_packet_size count of keys
29
+
30
+ ### trace
31
+ Proc for logging connection handling to Server
32
+
33
+
34
+
35
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+ RSpec::Core::RakeTask.new :spec
7
+ task :default => :spec
data/doc/ARCH.md ADDED
@@ -0,0 +1,54 @@
1
+ Client
2
+ ======
3
+
4
+ class Client
5
+
6
+ # returns a Head with three keys
7
+ # :timestamp (the value of last-modified header)
8
+ # :data (a nested Hash of translations)
9
+ # :status (the reponse status)
10
+ def initial_request
11
+ end
12
+
13
+ # returns true if redirected, false otherwise
14
+ def reporting_request(data)
15
+ end
16
+
17
+ # returns a Head with three keys
18
+ # :timestamp (the value of last-modified header)
19
+ # :data (a nested Hash of translations)
20
+ # :status (the reponse status)
21
+ def incremental_request
22
+ end
23
+
24
+ end
25
+
26
+ Initializer
27
+ ===========
28
+
29
+ options = {
30
+ :host => '',
31
+ :update_interval => 1.minute,
32
+ :reset_interval => 15.minutes,
33
+ :fallback => I18n.backend,
34
+ :api_key => '91885ca9ec4feb9b2ed2423cdbdeda32'
35
+ }
36
+ I18n.backend = GhostReader::I18nBackend.new(options).start_agents
37
+
38
+
39
+ Custom Initializer for Development
40
+ ==================================
41
+
42
+ require File.expand_path(File.join(%w(.. .. .. .. ghost_reader lib ghost_reader)), __FILE__)
43
+
44
+ config = {
45
+ :report_interval => 5, # secs
46
+ :retrieval_interval => 10, # secs
47
+ :fallback => I18n.backend,
48
+ :logfile => File.join(Rails.root, %w(log ghostwriter.log)),
49
+ :service => {
50
+ :api_key => '9d07cf6d805ea2951383c9ed76db762e' # Ghost Dummy Project
51
+ }
52
+ }
53
+
54
+ I18n.backend = GhostReader::Backend.new(config).spawn_agents
data/doc/SPEC.md ADDED
@@ -0,0 +1,138 @@
1
+ GhostReader/Writer protocol (initial draft)
2
+ ===========================================
3
+
4
+ # Purpose
5
+
6
+ The ghost/writer communication protocol should define the process for
7
+ efficient exchange of the missing i18n translations from the reader and
8
+ completed translations from the writer.
9
+
10
+ # Specification
11
+
12
+ ## Prerequisites
13
+
14
+ * The client/server communication should use HTTPS
15
+ * The communication should use JSON for exchanging data
16
+ * Identification should be performed by use of an API key
17
+ * Authentication/Authorization are not yet addressed
18
+
19
+ ## Use cases
20
+
21
+ * There are no requests specific to a locale. All request will update
22
+ or return translations for multiple locales.
23
+ * I18n keys may occur in two different forms:
24
+ - aggregated (string), e.g. `"this.is.a.sample.key"`
25
+ - nested (hash), e.g. (in JSON) `{"this":{"is":{"a":{"sample":{"key":null}}}}}`
26
+
27
+ ### Application start (initial request)
28
+
29
+ * On application start a request should be made from server to send the
30
+ already completed translations
31
+ - Use rails caching for case when multiple instances of the application are started
32
+ - This request has to be nonlocking/async, and must not hang or
33
+ crash the server if it fails
34
+ - The server performs caching on this request. (Note: Because of
35
+ different output formats it might make sense to caching on object
36
+ level.)
37
+ - The server must set the "Last-Modified" HTTP header.
38
+ - Request scheme: (1) `GET https://ghostwriter/api/<APIKEY>/translations`
39
+ - The client will track the "Last-Modified" time for use in
40
+ 'incremental requests'.
41
+ - This request will also respond to YAML & CSV for exporting.
42
+
43
+ ### Client reports missing translations (reporting request)
44
+
45
+ * The client should gather a collection of all missing translations.
46
+ - If a translation is missing the lookup will cascade into other
47
+ sources (I18n backends, like YAML files). (Note: This cannot be
48
+ achieved by chaining backends since fallbacks will not be
49
+ propagated.)
50
+ - If the lookup yields a result the result will reported along with
51
+ the key as a default value.
52
+
53
+ * The client should POST data for the missing translations.
54
+ - the server must respond with a Redirect-After-Post redirecting to (1)
55
+ - Request scheme: (2) `POST https://ghostwriter/api/<APIKEY>/translations`
56
+
57
+ * The server will validate the keys and create or update untranslated
58
+ entries.
59
+
60
+ ### Client recieves updated translations (incremental request)
61
+
62
+ * The client should GET data for updated translations
63
+ - The client must set the "If-Modified-Since" HTTP header. (Otherwise
64
+ the request equals the inital GET and all of the translations are sent.)
65
+ - The server will only send the transaltions that where updated
66
+ between the potint in time denoted by the "If-Modified-Since"
67
+ header and the time of the request (now).
68
+ - The server will set the "Last-Modified" HTTP header.
69
+ - The server will NOT perform any caching.
70
+ - Request scheme: (3) `GET https://ghostwriter/api/<APIKEY>/translations`,
71
+ with "If-Modified-Since" HTTP header set
72
+ * The client will merge the recieved data into it's current pool of
73
+ translations.
74
+ - Thereby the client will overwrite any conflicting translations
75
+ with the newly recieved data.
76
+ * The client will track the "Last-Modified" header for future requests.
77
+
78
+ ### Error handling
79
+
80
+ * The client should log the error and retry the request.
81
+ * After a number of retries the client should inform the administrator
82
+ (through email, sms... etc.) that there is a problem.
83
+
84
+ ## Data model definition
85
+
86
+ ### Request
87
+
88
+ * The request data should contain the following:
89
+ - locale code
90
+ - the i18n keys (aggregated) which where requested but have no
91
+ translation
92
+ - the default values if fallback lookups yielded a result
93
+ - a count, indicating how often they were requested (can be used
94
+ as a proxy variable for importance)
95
+ - timestamp when the last request was made
96
+ (as HTTP header "Last-Modified" -> "If-Modified-Since")
97
+ * Sample Request, reporting missing (JSON)
98
+
99
+ ```
100
+ {"sample.key_1":{"en":{"count":42,"default":"Sample translation 1."}},
101
+ "sample.key_2":{"en":{"count":23,"default":"Sample translation 2."}}}
102
+ ```
103
+
104
+ ### Response
105
+
106
+ * The response data should contain the following
107
+ - keys (nested) and their translations, nested in locales
108
+ - timestamp, when response was made ("Last-Modifed" HTTP header)
109
+ * Sample Response (JSON)
110
+
111
+ ```
112
+ {"en":{"sample":{"key_1":"Sample translation 1.","key_2":"Sample translation 2."}}}
113
+
114
+ ```
115
+
116
+ ## Lookup
117
+
118
+ The client as a I18n backend will lookup translations in multiple sources.
119
+
120
+ 1. Pool (maybe there is a better name for it)
121
+ - local
122
+ - maybe use Rails cache, to share pool over multiple instances
123
+ 2. GhostWriter API
124
+ - Asynchronously!
125
+ - As long as translations are missing, the client will poll the
126
+ server for updated translations and merge any newly recieved
127
+ translations into the local Pool.
128
+ 3. Fallback
129
+ - SimpleBackend (locale files)
130
+ - or whatever is used by the developers during coding
131
+
132
+ # Addendum
133
+
134
+ ## HTTP header date formats are specified here
135
+
136
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
137
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29
138
+ * e.g. in ruby strftime format '%a, %d %b %Y %H:%M:%S %Z'
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'excon'
3
+ require 'json'
4
+
5
+ # Excon.ssl_verify_peer = false
6
+
7
+ address = 'http://0.0.0.0:3000/api/91885ca9ec4feb9b2ed2423cdbdeda32/translations.json'
8
+ excon = Excon.new(address)
9
+ puts
10
+
11
+ puts "(1) Initial request... (GET without If-Modified-Since)"
12
+ response = excon.get
13
+ puts
14
+ puts " Status: #{response.status}"
15
+ puts " Body size: #{response.body.size}"
16
+ @last_modified = response.get_header('Last-Modified')
17
+ puts " Last-Modified: #{@last_modified}"
18
+ puts
19
+
20
+ puts "(2) Reporting request... (POST)"
21
+ data = {
22
+ "sample.key_1" => {"en" => {"count" => 42, "default" => "Sample translation 1."}},
23
+ "sample.key_2" => {"en" => {"count" => 23, "default" => "Sample translation 2."}}
24
+ }
25
+ response = excon.post(:body => "data=#{data.to_json}")
26
+ puts
27
+ puts " Status: #{response.status}"
28
+ puts
29
+
30
+ puts "Sleeping a second to avoid automatic 304"
31
+ sleep 1
32
+ puts
33
+
34
+ puts "(3) Incremental request... (GET with If-Modified-Since)"
35
+ headers = { 'If-Modified-Since' => @last_modified }
36
+ response = excon.get(:headers => headers)
37
+ puts
38
+ puts " Status: #{response.status}"
39
+ puts " Body size: #{response.body.size}"
40
+ puts
@@ -0,0 +1,35 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "ghost_reader/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "ghost_reader"
6
+ s.version = GhostReader::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Andreas König", "Phil Hofmann"]
9
+ s.email = ["koa@panter.ch", "phil@branch14.org"]
10
+ s.homepage = "https://github.com/branch14/ghost_reader"
11
+ s.summary = %q{i18n backend to ghost_writer service}
12
+ s.description = %q{Loads I18n-Yaml-Files via http and exchanges statistical data
13
+ and updates with the ghost_server}
14
+
15
+ s.rubyforge_project = "ghost_reader"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency('i18n')
23
+ s.add_dependency('json')
24
+ s.add_dependency('excon')
25
+
26
+ s.add_development_dependency('ruby-debug')
27
+ s.add_development_dependency('guard')
28
+ s.add_development_dependency('guard-rspec')
29
+
30
+ s.add_development_dependency('rack')
31
+ s.add_development_dependency('rake')
32
+ s.add_development_dependency('rspec')
33
+ s.add_development_dependency('mongrel')
34
+ s.add_development_dependency('actionpack', '3.0.7')
35
+ end
@@ -0,0 +1,161 @@
1
+ require 'logger'
2
+ require 'ostruct'
3
+ require 'i18n/backend/transliterator' # i18n/backend/base fails to require this
4
+ require 'i18n/backend/base'
5
+ require 'i18n/backend/memoize'
6
+ require 'i18n/backend/flatten'
7
+
8
+ module GhostReader
9
+ class Backend
10
+
11
+ module DebugLookup
12
+ def lookup(*args)
13
+ config.logger.debug "Lookup: #{args.inspect}"
14
+ # debugger
15
+ super
16
+ end
17
+ end
18
+
19
+ module Implementation
20
+
21
+ attr_accessor :config, :missings
22
+
23
+ # for options see code of default_config
24
+ def initialize(conf={})
25
+ self.config = OpenStruct.new(default_config.merge(conf))
26
+ yield(config) if block_given?
27
+ config.logger = Logger.new(config.logfile || STDOUT)
28
+ config.logger.level = config.log_level || Logger::WARN
29
+ config.service[:logger] ||= config.logger
30
+ config.client = Client.new(config.service)
31
+ config.logger.info "Initialized GhostReader backend."
32
+ end
33
+
34
+ def spawn_agents
35
+ config.logger.debug "GhostReader spawning agents."
36
+ spawn_retriever
37
+ spawn_reporter
38
+ config.logger.debug "GhostReader spawned its agents."
39
+ self
40
+ end
41
+
42
+ protected
43
+
44
+ # this won't be called if memoize kicks in
45
+ def lookup(locale, key, scope = [], options = {})
46
+ raise 'no fallback given' if config.fallback.nil?
47
+ config.fallback.translate(locale, key, options).tap do |result|
48
+ # TODO results which are hashes need to be tracked disaggregated
49
+ track({ key => { locale => { 'default' => result } } }) unless result.is_a?(Hash)
50
+ end
51
+ end
52
+
53
+ def track(missings)
54
+ return if self.missings.nil? # not yet initialized
55
+ self.missings.deep_merge!(missings)
56
+ end
57
+
58
+ def memoize_merge!(data, options={ :method => :merge! })
59
+ flattend = flatten_translations_for_all_locales(data)
60
+ symbolized_flattend = symbolize_keys(flattend)
61
+ memoized_lookup.send(options[:method], symbolized_flattend)
62
+ end
63
+
64
+ # performs initial and incremental requests
65
+ def spawn_retriever
66
+ config.logger.debug "Spawning retriever."
67
+ Thread.new do
68
+ begin
69
+ config.logger.debug "Performing initial request."
70
+ response = config.client.initial_request
71
+ memoize_merge! response[:data]
72
+ self.missings = {} # initialized
73
+ config.logger.info "Initial request successfull."
74
+ until false
75
+ begin
76
+ sleep config.retrieval_interval
77
+ response = config.client.incremental_request
78
+ if response[:status] == 200
79
+ config.logger.info "Incremental request with data."
80
+ config.logger.debug "Data: #{response[:data].inspect}"
81
+ memoize_merge! response[:data], :method => :deep_merge!
82
+ else
83
+ config.logger.debug "Incremental request, but no data."
84
+ end
85
+ rescue
86
+ config.logger.error "Exception in retriever loop: #{ex}"
87
+ end
88
+ end
89
+ rescue => ex
90
+ config.logger.error "Exception in retriever thread: #{ex}"
91
+ config.logger.debug ex.backtrace.join("\n")
92
+ end
93
+ end
94
+ end
95
+
96
+ # performs reporting requests
97
+ def spawn_reporter
98
+ config.logger.debug "Spawning reporter."
99
+ Thread.new do
100
+ until false
101
+ begin
102
+ sleep config.report_interval
103
+ unless self.missings.nil?
104
+ unless self.missings.empty?
105
+ config.logger.info "Reporting request with #{self.missings.keys.size} missings."
106
+ config.client.reporting_request(missings)
107
+ missings.clear
108
+ else
109
+ config.logger.debug "Reporting request omitted, nothing to report."
110
+ end
111
+ else
112
+ config.logger.debug "Reporting request omitted, not yet initialized, waiting for intial request."
113
+ end
114
+ rescue => ex
115
+ config.logger.error "Exception in reporter thread: #{ex}"
116
+ config.logger.debug ex.backtrace.join("\n")
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # a wrapper for I18n::Backend::Flatten#flatten_translations
123
+ def flatten_translations_for_all_locales(data)
124
+ data.inject({}) do |result, key_value|
125
+ begin
126
+ key, value = key_value
127
+ result.merge key => flatten_translations(key, value, true, false)
128
+ rescue ArgumentError => ae
129
+ config.logger.error "Error: #{ae}"
130
+ result
131
+ end
132
+ end
133
+ end
134
+
135
+ def symbolize_keys(hash)
136
+ hash.each.inject({}) do |symbolized_hash, key_value|
137
+ key, value = key_value
138
+ symbolized_hash.merge!({key.to_sym, value})
139
+ end
140
+ end
141
+
142
+ def default_config
143
+ {
144
+ :retrieval_interval => 15,
145
+ :report_interval => 10,
146
+ :fallback => nil, # a I18n::Backend (mandatory)
147
+ :logfile => nil, # a path
148
+ :log_level => nil, # Log level, the options are Config::(FATAL, ERROR, WARN, INFO and DEBUG) (http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html)
149
+ :service => {} # nested hash, see GhostReader::Client#default_config
150
+ }
151
+ end
152
+ end
153
+
154
+ include I18n::Backend::Base
155
+ include Implementation
156
+ include I18n::Backend::Memoize # provides @memoized_lookup
157
+ include I18n::Backend::Flatten # provides #flatten_translations
158
+ include DebugLookup
159
+ end
160
+ end
161
+
@@ -0,0 +1,91 @@
1
+ require 'excon'
2
+ require 'json'
3
+
4
+ module GhostReader
5
+ class Client
6
+
7
+ attr_accessor :config, :last_modified
8
+
9
+ def initialize(conf=nil)
10
+ self.config = OpenStruct.new(default_config.merge(conf || {}))
11
+ config.logger ||= Logger.new(config.logfile || STDOUT)
12
+ end
13
+
14
+ # returns a Head with three keys
15
+ # :timestamp (the value of last-modified header)
16
+ # :data (a nested Hash of translations)
17
+ # :status (the reponse status)
18
+ def initial_request
19
+ response = connect_with_retry
20
+ self.last_modified = response.get_header('Last-Modified')
21
+ build_head(response)
22
+ end
23
+
24
+ # returns true if redirected, false otherwise
25
+ def reporting_request(data)
26
+ response = connect_with_retry(:post, :body => "data=#{data.to_json}")
27
+ config.logger.error "Reporting request not redirected" unless response.status == 302
28
+ { :status => response.status }
29
+ end
30
+
31
+ # returns a Head with three keys
32
+ # :timestamp (the value of last-modified header)
33
+ # :data (a nested Hash of translations)
34
+ # :status (the reponse status)
35
+ def incremental_request
36
+ headers = { 'If-Modified-Since' => self.last_modified }
37
+ response = connect_with_retry(:get, :headers => headers)
38
+ self.last_modified = response.get_header('Last-Modified') if response.status == 200
39
+ build_head(response)
40
+ end
41
+
42
+ # this is just a wrapper to have a log message when the field is set
43
+ def last_modified=(value)
44
+ config.logger.debug "Last-Modified: #{value}"
45
+ @last_modified = value
46
+ end
47
+
48
+ private
49
+
50
+ def build_head(excon_response)
51
+ { :status => excon_response.status }.tap do |result|
52
+ result[:data] = JSON.parse(excon_response.body) if excon_response.status == 200
53
+ end
54
+ end
55
+
56
+ def service
57
+ @service ||= Excon.new(address)
58
+ end
59
+
60
+ def address
61
+ raise 'no api_key provided' if config.api_key.nil?
62
+ @address ||= config.uri.sub(':api_key', config.api_key)
63
+ end
64
+
65
+ # Wrapper method for retrying the connection
66
+ # :method - http method (post and get supported at the moment)
67
+ # :params - parameters sent to the service (excon)
68
+ def connect_with_retry(method = :get, params = {})
69
+ retries = self.config.connection_retries
70
+ while (retries > 0) do
71
+ response = service.send(method, params)
72
+
73
+ if response.status == 408
74
+ config.logger.error "Connection time-out. Retrying... #{retries}"
75
+ retries -= 1
76
+ else
77
+ retries = 0 # There is no timeout, no need to retry
78
+ end
79
+ end
80
+ response
81
+ end
82
+
83
+ def default_config
84
+ {
85
+ :uri => 'http://ghost.panter.ch/api/:api_key/translations.json',
86
+ :api_key => nil,
87
+ :connection_retries => 3
88
+ }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module GhostReader
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ %w(backend client).each do |f|
2
+ require File.expand_path(File.join(%w(.. ghost_reader), f), __FILE__)
3
+ end
4
+
@@ -0,0 +1,62 @@
1
+ namespace :ghost_reader do
2
+
3
+ desc "Fetch newest translations from ghost-writer and overwrite the local translations"
4
+ task :fetch => :environment do
5
+ unless I18n.backend.respond_to? :load_yaml_from_ghostwriter
6
+ raise "ERROR: Ghostwriter is not configured as I18n.backend"
7
+ end
8
+
9
+ begin
10
+ puts "Loading data from Ghostwriter and delete old translations"
11
+ yaml_data = I18n.backend.load_yaml_from_ghostwriter
12
+ rescue Exception => e
13
+ abort e.message
14
+ end
15
+
16
+ yaml_data.each_pair do |key,value|
17
+ outfile = Rails.root.join("config", "locales",
18
+ "#{key.to_s}.yml")
19
+ begin
20
+ puts "Deleting old translations: #{outfile}"
21
+ File.delete(outfile) if File.exists?(outfile)
22
+ rescue Exception => e
23
+ abort "Couldn't delete file: #{e.message}"
24
+ end
25
+
26
+ begin
27
+ puts "Writing new translations: #{outfile}"
28
+ File.open(outfile, "w") do |yaml_file|
29
+ yaml_file.write({key => value}.to_yaml)
30
+ end
31
+ rescue Exception => e
32
+ abort "Couldn't write file: #{e.message}"
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "Push all locally configured translations to ghost-writer"
38
+ task :push => :environment do
39
+ unless I18n.backend.respond_to? :push_all_backend_data
40
+ raise "ERROR: Ghostwriter is not configured as I18n.backend"
41
+ end
42
+ puts "Pushing data to Ghostwriter"
43
+ begin
44
+ I18n.backend.push_all_backend_data
45
+ rescue Exception => e
46
+ abort e.message
47
+ end
48
+ end
49
+
50
+ desc "Install default initializer for ghost reader"
51
+ task :install => :environment do
52
+
53
+ # TODO: this should work only when the ghost_reader is introduced as gem
54
+ infile = 'templates/ghost_reader.rb'
55
+ outdir = Rails.root.join("config", "initializers")
56
+
57
+ puts "Installing ghost_reader.rb initializer..."
58
+ FileUtils.copy_file(infile, outdir)
59
+ puts "Done."
60
+ end
61
+
62
+ end
@@ -0,0 +1,14 @@
1
+ # NOTE: We won't need this probably because the ghost_reader will be loaded as a gem
2
+ # require File.expand_path(File.join(%w(.. .. .. .. ghost_reader lib ghost_reader)), __FILE__)
3
+
4
+ config = {
5
+ :report_interval => 5, # secs
6
+ :retrieval_interval => 10, # secs
7
+ :fallback => I18n.backend,
8
+ :logfile => File.join(Rails.root, %w(log ghostwriter.log)),
9
+ :service => {
10
+ :api_key => '9d07cf6d805ea2951383c9ed76db762e' # Ghost Dummy Project
11
+ }
12
+ }
13
+
14
+ I18n.backend = GhostReader::Backend.new(config).spawn_agents