ghost_reader 1.0.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.
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