i18n_proofreading 1.0.1 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +6 -0
- data/Rakefile +9 -1
- data/app/controllers/i18n_proofreading/submissions_controller.rb +1 -1
- data/app/models/i18n_proofreading/suggestion.rb +9 -0
- data/docs/demo-640.mp4 +0 -0
- data/docs/demo.gif +0 -0
- data/docs/ractors.md +123 -0
- data/docs/screenshots/01-highlight.png +0 -0
- data/docs/screenshots/02-suggest.png +0 -0
- data/docs/screenshots/03-review-board.png +0 -0
- data/docs/screenshots/admin-ui/after-index-mobile.png +0 -0
- data/docs/screenshots/admin-ui/after-index.png +0 -0
- data/docs/screenshots/admin-ui/after-show-mobile.png +0 -0
- data/docs/screenshots/admin-ui/after-show.png +0 -0
- data/docs/screenshots/admin-ui/before-index.png +0 -0
- data/lib/i18n_proofreading/engine.rb +11 -0
- data/lib/i18n_proofreading/ractor_support.rb +518 -0
- data/lib/i18n_proofreading/version.rb +1 -1
- data/lib/i18n_proofreading/widget.rb +2 -2
- data/lib/i18n_proofreading.rb +1 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1dba859df20dd5bc929a6c3bbde753b36591b8dfd92fb24547272ab9d1137d72
|
|
4
|
+
data.tar.gz: 284954216db75a99209bb72248c2b447ac62dbd3d28b550c996015392422c4ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47cf7fd0e919c0f1be04b1b27804238da759b955339ee316d4fb522bdfd2e34b96aa661739bd9e8dff324d0a61d7127e155a2322f99b48d8d24736c69aac4003
|
|
7
|
+
data.tar.gz: ff614c4482c8c9ee666d6a636f46cc84d724e75289925dabf8aa10b27b004d98e31ce539715476faf7f155b6d2cf3d4b26a86899e281d255a449824c05f816fd
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.1.0] - 2026-10-18
|
|
6
|
+
|
|
7
|
+
- Add Rails edge CI and experimental Ruby 4.0+ Ractor request support. The host
|
|
8
|
+
must opt into `Rails.application.ractorize!` and use the upstream Rack/I18n
|
|
9
|
+
dependencies documented in `docs/ractors.md`.
|
|
10
|
+
- Exercise widget injection, suggestions, the read-only dashboard and request
|
|
11
|
+
callbacks through the mounted engine in worker Ractors.
|
|
12
|
+
- Prepare immutable widget assets and a frozen configuration snapshot at
|
|
13
|
+
Ractorization. Callbacks still run for each request, with isolated captures;
|
|
14
|
+
callbacks that capture mutable state fail at boot with the setting name.
|
|
15
|
+
Normal Rails boots retain mutable configuration and lazy asset loading.
|
|
16
|
+
- Bridge framework operations that are not yet Ractor safe (mounted URL
|
|
17
|
+
generation, digests/key derivation and attribute factories) to the main
|
|
18
|
+
Ractor. This adds dispatch overhead in Ractor mode; it is compatibility
|
|
19
|
+
support, not a claim of improved throughput. Existing Rails 7.1–8.1 installs
|
|
20
|
+
need no migration or initializer changes.
|
|
21
|
+
|
|
5
22
|
## [1.0.1] - 2026-08-11
|
|
6
23
|
|
|
7
24
|
- **A failing `on_submit` hook no longer turns a saved suggestion into a 500.**
|
data/README.md
CHANGED
|
@@ -357,3 +357,9 @@ touch your own CSS — and migrations that follow your app's `primary_key_type`.
|
|
|
357
357
|
## License
|
|
358
358
|
|
|
359
359
|
MIT.
|
|
360
|
+
|
|
361
|
+
## Rails edge and Ractors
|
|
362
|
+
|
|
363
|
+
Rails edge is tested alongside Rails 7.1–8.1. Experimental Ruby 4.0+ Ractor hosting
|
|
364
|
+
requires additional upstream dependencies and freezes configuration at startup.
|
|
365
|
+
See [the Ractor hosting guide](docs/ractors.md) for callbacks, requirements and checks.
|
data/Rakefile
CHANGED
|
@@ -10,7 +10,7 @@ require 'rake/testtask'
|
|
|
10
10
|
# Unit/integration tests — no browser, runs on every Ruby x Rails combo.
|
|
11
11
|
Rake::TestTask.new(:test) do |t|
|
|
12
12
|
t.libs << 'test'
|
|
13
|
-
t.test_files = FileList['test/**/*_test.rb'].exclude('test/system/**/*')
|
|
13
|
+
t.test_files = FileList['test/**/*_test.rb'].exclude('test/system/**/*', 'test/ractor/**/*')
|
|
14
14
|
t.warning = false
|
|
15
15
|
t.verbose = false
|
|
16
16
|
end
|
|
@@ -24,3 +24,11 @@ Rake::TestTask.new('test:system') do |t|
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
task default: :test
|
|
27
|
+
|
|
28
|
+
namespace :test do
|
|
29
|
+
desc 'Run isolated configuration and Rails edge Ractor request tests (Ruby 4.0+)'
|
|
30
|
+
task :ractor do
|
|
31
|
+
ruby '-Itest', 'test/ractor/configuration_test.rb'
|
|
32
|
+
ruby '-Itest', 'test/ractor/requests_test.rb'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -15,7 +15,7 @@ module I18nProofreading
|
|
|
15
15
|
# Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
|
|
16
16
|
# read once at boot, after the host's initializer.
|
|
17
17
|
if respond_to?(:rate_limit) && I18nProofreading.config.rate_limit
|
|
18
|
-
rate_limit(**I18nProofreading.config.rate_limit,
|
|
18
|
+
I18nProofreading::RactorSupport.rate_limit(self, **I18nProofreading.config.rate_limit,
|
|
19
19
|
only: :create,
|
|
20
20
|
with: lambda {
|
|
21
21
|
render json: { errors: ['Too many suggestions. Please slow down and try again.'] },
|
|
@@ -13,6 +13,15 @@ module I18nProofreading
|
|
|
13
13
|
|
|
14
14
|
enum :status, STATUSES.index_by(&:itself), prefix: :status, default: :pending
|
|
15
15
|
|
|
16
|
+
# Explicit methods work in non-main Ractors; Rails' enum-generated
|
|
17
|
+
# predicates and bang methods are currently unisolated closures.
|
|
18
|
+
def status_pending? = status_for_database == 'pending'
|
|
19
|
+
def status_pending! = update!(status: 'pending')
|
|
20
|
+
def status_applied? = status_for_database == 'applied'
|
|
21
|
+
def status_applied! = update!(status: 'applied')
|
|
22
|
+
def status_rejected? = status_for_database == 'rejected'
|
|
23
|
+
def status_rejected! = update!(status: 'rejected')
|
|
24
|
+
|
|
16
25
|
validates :translation_key, presence: true, length: { maximum: 500 }
|
|
17
26
|
validates :proposed_value, presence: true, length: { maximum: 5_000 }
|
|
18
27
|
validates :old_value, length: { maximum: 5_000 }
|
data/docs/demo-640.mp4
ADDED
|
Binary file
|
data/docs/demo.gif
ADDED
|
Binary file
|
data/docs/ractors.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Rails edge and experimental Ractor hosting
|
|
2
|
+
|
|
3
|
+
The regular CI matrix covers Rails 7.1, 7.2, 8.0 and 8.1. A required Rails
|
|
4
|
+
edge job runs the same suite against `rails/rails` main. A separate job boots
|
|
5
|
+
an eager-loaded host, calls `Rails.application.ractorize!`, and sends real
|
|
6
|
+
Rack requests through the mounted engine in non-main Ractors, including
|
|
7
|
+
simultaneous requests. The Ractor checks run in separate processes because
|
|
8
|
+
Ractorization freezes Rails; they never freeze the ordinary test application.
|
|
9
|
+
|
|
10
|
+
## Host prerequisites
|
|
11
|
+
|
|
12
|
+
Ractor hosting currently requires Ruby 4.0+, Rails main, Rack main's
|
|
13
|
+
`rack/ractorize`, and the unreleased [I18n Ractor support](https://github.com/ruby-i18n/i18n/pull/741).
|
|
14
|
+
`gemfiles/rails_ractor.gemfile` records the exact I18n revision tested. These
|
|
15
|
+
are development/test dependencies, not new runtime dependencies for released
|
|
16
|
+
Rails. The gem's supported dependency range remains Rails >= 7.1 and < 9.
|
|
17
|
+
|
|
18
|
+
For a host experimenting with Ractor hosting, the tested upstream overrides are:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "rails", github: "rails/rails", branch: "main"
|
|
22
|
+
gem "rack", github: "rack/rack", branch: "main"
|
|
23
|
+
gem "i18n", github: "Shopify/i18n", ref: "58aa1dcd3e04c34831e1c2ec06c83391559c7c91"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Follow Rails' own Ractor hosting requirements: disable reloading, eager-load
|
|
27
|
+
application code, use a shareable logger/cache and a Ractor-compatible job
|
|
28
|
+
adapter. Before application classes load, enable Rails' callback isolation:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "active_support/ractors"
|
|
32
|
+
ActiveSupport::Ractors.unshareable_proc_action = :raise
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Our isolated fixture uses `ActiveSupport::Ractors::Logger`, a null
|
|
36
|
+
cache, and (where jobs are loaded) `:inline`. Its separate shared counter proves
|
|
37
|
+
that public submission limits remain effective across worker Ractors. The
|
|
38
|
+
ordinary dummy application's `:test` job adapter is unchanged.
|
|
39
|
+
|
|
40
|
+
The engine prepares itself automatically on `before_ractorize`; ordinary Rails
|
|
41
|
+
boots do not freeze its configuration or load the experimental I18n extension.
|
|
42
|
+
An unavailable I18n Ractor extension produces a startup error explaining the
|
|
43
|
+
prerequisite. Rails edge and Ractor hosting remain experimental: this is not a
|
|
44
|
+
promise that every third-party host middleware, database driver, storage
|
|
45
|
+
service, mail adapter or Action Cable adapter is Ractor safe.
|
|
46
|
+
|
|
47
|
+
## Configuration and callbacks
|
|
48
|
+
|
|
49
|
+
Finish configuring I18nProofreading before calling `ractorize!`. The engine publishes
|
|
50
|
+
a frozen copy of the configuration and warms/freezes widget JS, CSS and content
|
|
51
|
+
fingerprints. Changes after Ractorization require a process restart. Arrays and
|
|
52
|
+
hashes supplied by the host are copied before freezing.
|
|
53
|
+
|
|
54
|
+
Callbacks are isolated with Ruby's `Ractor.shareable_lambda` or
|
|
55
|
+
`Ractor.shareable_proc`, preserving lambda/proc arity. They continue to receive
|
|
56
|
+
the same request, user, record or event arguments and run on each request.
|
|
57
|
+
Isolated callbacks use arguments and constants; their implicit initializer
|
|
58
|
+
`self` becomes nil, following Rails' isolated callback convention.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
I18nProofreading.configure do |config|
|
|
62
|
+
config.enabled = ->(request) { request.get_header("HTTP_X_WIDGET") != "off" }
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Immutable captured values must already be shareable:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
expected_host = "app.example.com".freeze
|
|
70
|
+
I18nProofreading.configure do |config|
|
|
71
|
+
config.enabled = ->(request) { request.host == expected_host }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Mutable captures (an array collecting notifications, an unfrozen captured
|
|
76
|
+
string, or reassigned outer locals) cannot be shared. The engine fails at
|
|
77
|
+
startup with `I18nProofreading.config.<setting>` in the error rather than failing on
|
|
78
|
+
a visitor's request. Use request-local state and external persistence for
|
|
79
|
+
side effects. A pre-isolated callback made with an explicit shareable `self:`
|
|
80
|
+
keeps that receiver. Stateful callable objects must be copyable/shareable;
|
|
81
|
+
custom objects remain subject to Ruby's Ractor rules.
|
|
82
|
+
|
|
83
|
+
## Scope and cost
|
|
84
|
+
|
|
85
|
+
Some Rails main paths are still being converted to Ractors. The engine warms
|
|
86
|
+
model attributes and controller names, prepares ETag callbacks, and supplies
|
|
87
|
+
narrow bridges for mounted URL generation, cache freshness, digest/key
|
|
88
|
+
operations, GlobalID URI decoding and boot-time attribute decorators. Those bridges preserve the
|
|
89
|
+
original operation on the main Ractor; requests, authorization callbacks,
|
|
90
|
+
rendering and persistence orchestration execute in their worker Ractors.
|
|
91
|
+
Adapter name quoting delegates to its original implementation on the main Ractor,
|
|
92
|
+
which preserves escaping and avoids process-global mutable caches in workers.
|
|
93
|
+
Disk-backed uploads use Ractor-local storage clients and plain temporary files
|
|
94
|
+
with explicit cleanup, because Tempfile's generated delegation methods cannot
|
|
95
|
+
be called from another Ractor. MIME registrations must finish before
|
|
96
|
+
Ractorization, when Marcel's MIME tables become immutable. Eager model schema
|
|
97
|
+
preparation adds startup database work.
|
|
98
|
+
The bridges are internal compatibility code, add coordination overhead, and
|
|
99
|
+
must be reviewed as upstream Rails evolves. No throughput improvement is claimed.
|
|
100
|
+
|
|
101
|
+
## Run the checks
|
|
102
|
+
|
|
103
|
+
Use the Ruby configured in `mise.toml`, never macOS's system Ruby.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install
|
|
107
|
+
BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle exec rake test
|
|
108
|
+
BUNDLE_GEMFILE=gemfiles/rails_ractor.gemfile bundle install
|
|
109
|
+
BUNDLE_GEMFILE=gemfiles/rails_ractor.gemfile bundle exec rake test:ractor
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The request fixture creates and removes its own temporary database and host;
|
|
113
|
+
it does not reuse or freeze `test/dummy`. The shared schema lives in
|
|
114
|
+
`test/support/schema.rb` so the ordinary and Ractor suites exercise the same
|
|
115
|
+
record shape. Successful checks establish the tested SQLite host and explicit
|
|
116
|
+
request paths, not all combinations of host integrations.
|
|
117
|
+
|
|
118
|
+
The media job also runs with `RACTOR_MEDIA=1`, exercising private disk-backed
|
|
119
|
+
Active Storage downloads through the engine's authorization gate. Storage
|
|
120
|
+
services are initialized per Ractor, and streamed responses use a thread local
|
|
121
|
+
to that Ractor. Livechat additionally runs with `RACTOR_CABLE=1`, checking the
|
|
122
|
+
signed stream token and broadcasts delegated to the main Action Cable server.
|
|
123
|
+
Other storage services and production Cable adapters need host-specific QA.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module I18nProofreading
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
|
+
initializer 'i18n_proofreading.ractor_middleware' do |app|
|
|
6
|
+
app.middleware.insert_before 0, RactorSupport::Middleware
|
|
7
|
+
end
|
|
8
|
+
|
|
5
9
|
isolate_namespace I18nProofreading
|
|
6
10
|
|
|
11
|
+
initializer 'i18n_proofreading.ractorize', before: 'action_controller.ractorize' do
|
|
12
|
+
if Rails::Application.method_defined?(:ractorize!)
|
|
13
|
+
Engine.routes.singleton_class.prepend(RactorSupport::MountedRoutes)
|
|
14
|
+
end
|
|
15
|
+
ActiveSupport.on_load(:before_ractorize) { I18nProofreading::RactorSupport.prepare! }
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
rake_tasks do
|
|
8
19
|
load File.expand_path('../tasks/i18n_proofreading_tasks.rake', __dir__)
|
|
9
20
|
end
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18nProofreading
|
|
4
|
+
# Used only by Rails' opt-in before_ractorize hook. Normal Rails boots keep
|
|
5
|
+
# mutable configuration and lazy assets, including on older Ruby versions.
|
|
6
|
+
module RactorSupport
|
|
7
|
+
# Small bridges for framework paths that Rails main does not prepare yet.
|
|
8
|
+
# They are installed only when the host explicitly calls ractorize!.
|
|
9
|
+
# Rails' built-in JSON renderer is still defined from an unisolated Proc.
|
|
10
|
+
# Keep its JSON/JSONP and escaping behavior in a regular Ruby method.
|
|
11
|
+
module JsonRendering
|
|
12
|
+
def _render_with_renderer_json(json, options)
|
|
13
|
+
json_options = options.except(:callback, :content_type, :status)
|
|
14
|
+
json_options[:escape] ||= false if !self.class.escape_json_responses? && options[:callback].blank?
|
|
15
|
+
json = json.to_json(json_options) unless json.is_a?(String)
|
|
16
|
+
if options[:callback].present?
|
|
17
|
+
self.content_type = :js if media_type.nil? || media_type == Mime[:json]
|
|
18
|
+
"/**/#{options[:callback]}(#{json})"
|
|
19
|
+
else
|
|
20
|
+
self.content_type = :json if media_type.nil?
|
|
21
|
+
json
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module DependencyInterlock
|
|
27
|
+
def interlock
|
|
28
|
+
return super if Ractor.main?
|
|
29
|
+
|
|
30
|
+
Ractor[:engine_dependency_interlock] ||= ActiveSupport::Dependencies::Interlock.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module LiveThreads
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def new_controller_thread
|
|
38
|
+
return super if Ractor.main?
|
|
39
|
+
|
|
40
|
+
Thread.new do
|
|
41
|
+
Thread.current.abort_on_exception = true
|
|
42
|
+
yield
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module UniquenessCoverage
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def covered_by_unique_index?(klass, record, attribute, scope)
|
|
51
|
+
return super unless frozen?
|
|
52
|
+
|
|
53
|
+
copies = Ractor[:engine_uniqueness_validators] ||= {}
|
|
54
|
+
copy = copies[[self, klass]] ||= dup
|
|
55
|
+
copy.send(:covered_by_unique_index?, klass, record, attribute, scope)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# GlobalID calls CGI.unescape without an encoding, whose default reads a
|
|
60
|
+
# process-wide class variable. Preserve CGI's configured default on main.
|
|
61
|
+
module UriUnescaping
|
|
62
|
+
def unescape(string, *arguments)
|
|
63
|
+
return super if Ractor.main?
|
|
64
|
+
|
|
65
|
+
shared_arguments = Ractor.make_shareable([string, *arguments], copy: true)
|
|
66
|
+
ActiveSupport::Ractors.on_main(self) { unescape(*shared_arguments) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module CacheFreshness
|
|
71
|
+
def strict_freshness
|
|
72
|
+
return super if Ractor.main?
|
|
73
|
+
|
|
74
|
+
ActiveSupport::Ractors.on_main(self) { strict_freshness }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Attribute decorators (including Rails enums and serialized attributes)
|
|
79
|
+
# are boot-time factories. Keep their original receivers and captures on
|
|
80
|
+
# the main Ractor, rather than changing the semantics of Rails' closures.
|
|
81
|
+
class Decorator
|
|
82
|
+
def initialize(callback)
|
|
83
|
+
@key = RactorSupport.register_decorator(callback)
|
|
84
|
+
freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def call(*arguments)
|
|
88
|
+
return RactorSupport.call_decorator(@key, arguments) if Ractor.main?
|
|
89
|
+
|
|
90
|
+
shared_arguments = Ractor.make_shareable(arguments, copy: true)
|
|
91
|
+
ActiveSupport::Ractors.on_main(self) { RactorSupport.call_decorator(@key, shared_arguments) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.register_decorator(callback)
|
|
96
|
+
@decorators ||= []
|
|
97
|
+
@decorators << callback
|
|
98
|
+
@decorators.length - 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.call_decorator(key, arguments)
|
|
102
|
+
@decorators.fetch(key).call(*arguments)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.prepare_attributes(model)
|
|
106
|
+
prepare_attributes(model.superclass) if model.superclass.respond_to?(:pending_attribute_modifications, true)
|
|
107
|
+
model.send(:pending_attribute_modifications)&.each do |modification|
|
|
108
|
+
next unless modification.respond_to?(:decorator) && modification.decorator.is_a?(Proc)
|
|
109
|
+
|
|
110
|
+
modification.decorator = Decorator.new(modification.decorator)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
module AdapterQuoting
|
|
115
|
+
def quote_column_name(name)
|
|
116
|
+
return super if Ractor.main?
|
|
117
|
+
|
|
118
|
+
shared_name = Ractor.make_shareable(name, copy: true)
|
|
119
|
+
ActiveSupport::Ractors.on_main(self) { quote_column_name(shared_name) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def quote_table_name(name)
|
|
123
|
+
return super if Ractor.main?
|
|
124
|
+
|
|
125
|
+
shared_name = Ractor.make_shareable(name, copy: true)
|
|
126
|
+
ActiveSupport::Ractors.on_main(self) { quote_table_name(shared_name) }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Route arguments can contain records with native Set caches. Only their
|
|
131
|
+
# to_param value is needed to generate a mount prefix; keep the caller's
|
|
132
|
+
# original record objects in the options passed to the actual route.
|
|
133
|
+
def self.routing_snapshot(value)
|
|
134
|
+
return value if Ractor.shareable?(value)
|
|
135
|
+
|
|
136
|
+
copy = case value
|
|
137
|
+
when ActiveRecord::Base then value.to_param
|
|
138
|
+
when Hash then value.to_h { |key, item| [routing_snapshot(key), routing_snapshot(item)] }
|
|
139
|
+
when Array then value.map { |item| routing_snapshot(item) }
|
|
140
|
+
when Set then Set.new(value.map { |item| routing_snapshot(item) })
|
|
141
|
+
else return Ractor.make_shareable(value, copy: true)
|
|
142
|
+
end
|
|
143
|
+
Ractor.make_shareable(copy)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
module MountedRoutes
|
|
147
|
+
def find_script_name(options)
|
|
148
|
+
return super unless defined?(Ractor.shareable_proc) && !Ractor.main?
|
|
149
|
+
|
|
150
|
+
shared_options = RactorSupport.routing_snapshot(options)
|
|
151
|
+
prefix, updated = ActiveSupport::Ractors.on_main(self) do
|
|
152
|
+
local_options = shared_options.dup
|
|
153
|
+
[find_script_name(local_options), local_options]
|
|
154
|
+
end
|
|
155
|
+
(shared_options.keys - updated.keys).each { |key| options.delete(key) }
|
|
156
|
+
updated.each do |key, value|
|
|
157
|
+
options[key] = value unless shared_options.key?(key) && shared_options[key] == value
|
|
158
|
+
end
|
|
159
|
+
prefix
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
module Digesting
|
|
164
|
+
def hexdigest(value)
|
|
165
|
+
return super if Ractor.main?
|
|
166
|
+
|
|
167
|
+
shared_value = Ractor.make_shareable(value, copy: true)
|
|
168
|
+
ActiveSupport::Ractors.on_main(self) { hexdigest(shared_value) }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
module KeyGeneration
|
|
173
|
+
def generate_key(salt, key_size = 64)
|
|
174
|
+
return super if Ractor.main?
|
|
175
|
+
|
|
176
|
+
shared_salt = Ractor.make_shareable(salt, copy: true)
|
|
177
|
+
ActiveSupport::Ractors.on_main(self) { generate_key(shared_salt, key_size) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
module UploadCleanup
|
|
182
|
+
def open
|
|
183
|
+
reopen(path, 'rb+')
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Tempfile#close takes a positional unlink flag.
|
|
188
|
+
def close(unlink_now = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
189
|
+
super() unless closed?
|
|
190
|
+
unlink if unlink_now
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def unlink
|
|
194
|
+
File.unlink(path)
|
|
195
|
+
rescue Errno::ENOENT
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def close!
|
|
200
|
+
close(true)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def self.upload_tempfile(filename, _content_type)
|
|
205
|
+
extension = File.extname(filename.gsub("\0", '%00'))[0, 129]
|
|
206
|
+
Tempfile.create(['RackMultipart', extension]).extend(UploadCleanup)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class Middleware
|
|
210
|
+
def initialize(app)
|
|
211
|
+
@app = app
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def call(env)
|
|
215
|
+
if (fallbacks = RactorSupport.fallbacks)
|
|
216
|
+
Fiber[:i18n_fallbacks] ||= fallbacks
|
|
217
|
+
env['rack.multipart.tempfile_factory'] ||= RactorSupport.method(:upload_tempfile) unless Ractor.main?
|
|
218
|
+
end
|
|
219
|
+
@app.call(env)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.fallbacks
|
|
224
|
+
@fallbacks
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
module StorageMetadata
|
|
228
|
+
def analyzed = read_store_attribute(:metadata, :analyzed)
|
|
229
|
+
def identified = read_store_attribute(:metadata, :identified)
|
|
230
|
+
def composed = read_store_attribute(:metadata, :composed)
|
|
231
|
+
|
|
232
|
+
def analyzed=(value)
|
|
233
|
+
write_store_attribute(:metadata, :analyzed, value)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def identified=(value)
|
|
237
|
+
write_store_attribute(:metadata, :identified, value)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def composed=(value)
|
|
241
|
+
write_store_attribute(:metadata, :composed, value)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
class StorageServices
|
|
246
|
+
def initialize(configurations)
|
|
247
|
+
@configurations = Ractor.make_shareable(configurations, copy: true)
|
|
248
|
+
freeze
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def fetch(name, &)
|
|
252
|
+
registry = if Ractor.main?
|
|
253
|
+
RactorSupport.storage_registry
|
|
254
|
+
else
|
|
255
|
+
Ractor[:engine_storage_services] ||= ActiveStorage::Service::Registry.new(@configurations)
|
|
256
|
+
end
|
|
257
|
+
registry.fetch(name, &)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def self.storage_registry
|
|
262
|
+
@storage_registry
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
module StorageChecksums
|
|
266
|
+
def checksum_implementation(**options)
|
|
267
|
+
implementation = super
|
|
268
|
+
if Ractor.main? || !implementation.respond_to?(:name) || !implementation.name&.start_with?('OpenSSL::Digest::')
|
|
269
|
+
return implementation
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
DigestFactory.new(implementation.name.delete_prefix('OpenSSL::Digest::'))
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
class DigestFactory
|
|
277
|
+
def initialize(algorithm)
|
|
278
|
+
@algorithm = algorithm.freeze
|
|
279
|
+
freeze
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def new
|
|
283
|
+
OpenSSL::Digest.new(@algorithm)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def file(io)
|
|
287
|
+
new.file(io)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def base64digest(value)
|
|
291
|
+
new.base64digest(value)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.prepare_redirects!
|
|
296
|
+
base = ActionController::Base
|
|
297
|
+
readers = Module.new
|
|
298
|
+
%i[raise_on_open_redirects action_on_open_redirect action_on_path_relative_redirect].each do |key|
|
|
299
|
+
source = base.method(key).source_location&.first
|
|
300
|
+
next unless source&.end_with?('/action_controller/metal/redirecting.rb')
|
|
301
|
+
|
|
302
|
+
value = Ractor.make_shareable(base.public_send(key), copy: true)
|
|
303
|
+
readers.define_method(key, Ractor.shareable_proc { Ractor.main? ? super() : value })
|
|
304
|
+
end
|
|
305
|
+
return if readers.instance_methods(false).empty?
|
|
306
|
+
|
|
307
|
+
base.prepend(readers)
|
|
308
|
+
base.singleton_class.prepend(readers)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def self.prepare_previewers!
|
|
312
|
+
ActiveStorage.previewers.each do |previewer|
|
|
313
|
+
readers = Module.new
|
|
314
|
+
previewer.methods.grep(/_exists\?\z/).each do |capability|
|
|
315
|
+
method = previewer.method(capability)
|
|
316
|
+
next unless method.arity.zero? && method.source_location&.first&.include?('/active_storage/previewer/')
|
|
317
|
+
|
|
318
|
+
readers.define_method(capability, Ractor.shareable_proc do
|
|
319
|
+
if Ractor.main?
|
|
320
|
+
super()
|
|
321
|
+
else
|
|
322
|
+
ActiveSupport::Ractors.on_main(self) { public_send(capability) }
|
|
323
|
+
end
|
|
324
|
+
end)
|
|
325
|
+
end
|
|
326
|
+
previewer.singleton_class.prepend(readers) unless readers.instance_methods(false).empty?
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def self.prepare_storage!
|
|
331
|
+
return unless defined?(ActiveStorage::Blob)
|
|
332
|
+
|
|
333
|
+
prepare_previewers!
|
|
334
|
+
ActiveStorage::Blob.prepend(StorageMetadata) unless ActiveStorage::Blob < StorageMetadata
|
|
335
|
+
%i[EXTENSIONS TYPE_EXTS TYPE_ALIASES TYPE_PARENTS ROOT_XML MAGIC].each do |name|
|
|
336
|
+
Ractor.make_shareable(Marcel.const_get(name)) if Marcel.const_defined?(name)
|
|
337
|
+
end
|
|
338
|
+
ActiveStorage::Service.prepend(StorageChecksums) unless ActiveStorage::Service < StorageChecksums
|
|
339
|
+
unless Ractor.shareable?(ActiveStorage::Blob.services)
|
|
340
|
+
@storage_registry = ActiveStorage::Blob.services
|
|
341
|
+
configurations = @storage_registry.instance_variable_get(:@configurations)
|
|
342
|
+
default_name = ActiveStorage::Blob.service.name
|
|
343
|
+
default_reader = Module.new
|
|
344
|
+
default_reader.define_method(:service, Ractor.shareable_proc do
|
|
345
|
+
Ractor.main? ? super() : services.fetch(default_name)
|
|
346
|
+
end)
|
|
347
|
+
ActiveStorage::Blob.singleton_class.prepend(default_reader)
|
|
348
|
+
ActiveStorage::Blob.services = StorageServices.new(configurations)
|
|
349
|
+
end
|
|
350
|
+
settings = ActiveStorage.class_variables.map { |name| name.to_s.delete_prefix('@@').to_sym }
|
|
351
|
+
settings.select! { |key| ActiveStorage.respond_to?(key) }
|
|
352
|
+
snapshot = settings.to_h { |key| [key, ActiveStorage.public_send(key)] }
|
|
353
|
+
checksum = snapshot[:checksum_implementation]
|
|
354
|
+
if checksum.is_a?(Class) && checksum.name&.start_with?('OpenSSL::Digest::')
|
|
355
|
+
snapshot[:checksum_implementation] = DigestFactory.new(checksum.name.delete_prefix('OpenSSL::Digest::'))
|
|
356
|
+
end
|
|
357
|
+
Ractor.make_shareable(snapshot)
|
|
358
|
+
readers = Module.new
|
|
359
|
+
settings.each do |key|
|
|
360
|
+
readers.define_method(key, Ractor.shareable_proc { Ractor.main? ? super() : snapshot.fetch(key) })
|
|
361
|
+
end
|
|
362
|
+
ActiveStorage.singleton_class.prepend(readers)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def self.prepare_framework!
|
|
366
|
+
CGI.singleton_class.prepend(UriUnescaping) unless CGI.singleton_class < UriUnescaping
|
|
367
|
+
Ractor.make_shareable(GlobalID.app) if defined?(GlobalID) && GlobalID.respond_to?(:app)
|
|
368
|
+
I18n.available_locales.each { |locale| I18n.transliterate('ractor', locale: locale) }
|
|
369
|
+
require 'i18n/ractorize'
|
|
370
|
+
require 'tempfile'
|
|
371
|
+
prepare_redirects!
|
|
372
|
+
%i[BASE36_ALPHABET BASE58_ALPHABET].each do |name|
|
|
373
|
+
Ractor.make_shareable(SecureRandom.const_get(name)) if SecureRandom.const_defined?(name)
|
|
374
|
+
end
|
|
375
|
+
uniqueness = ActiveRecord::Validations::UniquenessValidator
|
|
376
|
+
uniqueness.prepend(UniquenessCoverage) unless uniqueness < UniquenessCoverage
|
|
377
|
+
dependencies = ActiveSupport::Dependencies.singleton_class
|
|
378
|
+
dependencies.prepend(DependencyInterlock) unless dependencies < DependencyInterlock
|
|
379
|
+
prepare_storage!
|
|
380
|
+
Ractor.make_shareable(ActiveRecord::Associations::AssociationScope::INSTANCE)
|
|
381
|
+
Ractor.make_shareable(ActionDispatch::Http::Cache::Response::SPECIAL_KEYS)
|
|
382
|
+
freshness = ActionDispatch::Http::Cache::Request.singleton_class
|
|
383
|
+
freshness.prepend(CacheFreshness) unless freshness < CacheFreshness
|
|
384
|
+
fallbacks = I18n.fallbacks
|
|
385
|
+
I18n.available_locales.each { |locale| fallbacks[locale] }
|
|
386
|
+
@fallbacks = Ractor.make_shareable(fallbacks)
|
|
387
|
+
ActiveSupport::Digest.singleton_class.prepend(Digesting) unless ActiveSupport::Digest.singleton_class < Digesting
|
|
388
|
+
ActiveSupport::KeyGenerator.prepend(KeyGeneration) unless ActiveSupport::KeyGenerator < KeyGeneration
|
|
389
|
+
ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
|
|
390
|
+
adapter = pool.db_config.adapter_class.singleton_class
|
|
391
|
+
adapter.prepend(AdapterQuoting) unless adapter < AdapterQuoting
|
|
392
|
+
end
|
|
393
|
+
rescue LoadError => e
|
|
394
|
+
raise LoadError,
|
|
395
|
+
"Ractor requests need the upstream I18n Ractor support (see gemfiles/rails_ractor.gemfile): #{e.message}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def self.prepare!
|
|
399
|
+
# A cache store can reference the whole middleware stack and freeze
|
|
400
|
+
# other mounted engines before their own hooks run. Warm every engine's
|
|
401
|
+
# request environment before making any of that graph shareable.
|
|
402
|
+
Rails.application.railties.each do |railtie|
|
|
403
|
+
railtie.env_config if railtie.is_a?(Rails::Engine)
|
|
404
|
+
end
|
|
405
|
+
Engine.instance.env_config
|
|
406
|
+
prepare_framework!
|
|
407
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
408
|
+
next if model.abstract_class? || !model.table_exists?
|
|
409
|
+
|
|
410
|
+
model.reflect_on_all_associations.each { |reflection| reflection.send(:inverse_name) }
|
|
411
|
+
prepare_attributes(model)
|
|
412
|
+
Ractor.make_shareable(model.defined_enums)
|
|
413
|
+
model.defined_enums.each do |attribute, mapping|
|
|
414
|
+
method_name = attribute.pluralize
|
|
415
|
+
source = model.method(method_name).source_location&.first
|
|
416
|
+
next unless source&.end_with?('/active_record/enum.rb')
|
|
417
|
+
|
|
418
|
+
model.define_singleton_method(method_name, Ractor.shareable_proc { mapping })
|
|
419
|
+
end
|
|
420
|
+
model.define_attribute_methods
|
|
421
|
+
if model.respond_to?(:attachment_reflections)
|
|
422
|
+
model.attachment_reflections.each_value(&:named_variants)
|
|
423
|
+
Ractor.make_shareable(model.attachment_reflections)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
ActionController::Base.descendants.each do |controller|
|
|
427
|
+
next unless controller.name&.start_with?('I18nProofreading::')
|
|
428
|
+
|
|
429
|
+
controller.prepend(LiveThreads) if defined?(ActionController::Live) && controller < ActionController::Live
|
|
430
|
+
renderer_source = controller.instance_method(:_render_with_renderer_json).source_location&.first
|
|
431
|
+
if renderer_source&.end_with?('/action_controller/metal/renderers.rb') && !(controller < JsonRendering)
|
|
432
|
+
controller.prepend(JsonRendering)
|
|
433
|
+
end
|
|
434
|
+
Ractor.make_shareable(controller.controller_name)
|
|
435
|
+
controller.etaggers = controller.etaggers.map { |callback| Ractor.shareable_proc(&callback) }.freeze
|
|
436
|
+
end
|
|
437
|
+
I18nProofreading.instance_variable_set(:@config, configuration(I18nProofreading.config))
|
|
438
|
+
%i[javascript dashboard_javascript dashboard_stylesheet fingerprint
|
|
439
|
+
dashboard_fingerprint dashboard_stylesheet_fingerprint].each do |method|
|
|
440
|
+
next unless Widget.respond_to?(method)
|
|
441
|
+
|
|
442
|
+
Widget.public_send(method)
|
|
443
|
+
end
|
|
444
|
+
ActionController::Base.descendants.each do |controller|
|
|
445
|
+
next unless controller.respond_to?(:i18n_proofreading_rate_limit_options)
|
|
446
|
+
|
|
447
|
+
options = controller.i18n_proofreading_rate_limit_options
|
|
448
|
+
controller.i18n_proofreading_rate_limit_options = share_options(options)
|
|
449
|
+
end
|
|
450
|
+
Widget.instance_variables.each do |variable|
|
|
451
|
+
Widget.instance_variable_set(variable, Ractor.make_shareable(Widget.instance_variable_get(variable)))
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Rails edge's rate_limit currently closes over reassigned keyword
|
|
456
|
+
# arguments. A named filter avoids that unshareable closure; the actual
|
|
457
|
+
# counter, cache key and rejection still use Rails' rate_limiting method.
|
|
458
|
+
def self.rate_limit(controller, to:, within:, only:, with:, **options)
|
|
459
|
+
unless Rails::Application.method_defined?(:ractorize!)
|
|
460
|
+
return controller.rate_limit(to: to, within: within, only: only, with: with, **options)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
controller.class_attribute :i18n_proofreading_rate_limit_options, instance_accessor: false
|
|
464
|
+
controller.i18n_proofreading_rate_limit_options = {
|
|
465
|
+
to: to, within: within, by: -> { request.remote_ip }, with: with,
|
|
466
|
+
store: controller.cache_store, name: nil, scope: controller.controller_path
|
|
467
|
+
}.merge(options.slice(:by, :store, :name, :scope))
|
|
468
|
+
controller.i18n_proofreading_rate_limit_options[:scope] ||= controller.controller_path
|
|
469
|
+
filters = options.except(:by, :store, :name, :scope)
|
|
470
|
+
controller.before_action :i18n_proofreading_rate_limit, only: only, **filters
|
|
471
|
+
controller.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
472
|
+
private
|
|
473
|
+
def i18n_proofreading_rate_limit
|
|
474
|
+
rate_limiting(**self.class.i18n_proofreading_rate_limit_options)
|
|
475
|
+
end
|
|
476
|
+
RUBY
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def self.share_options(options)
|
|
480
|
+
options.to_h do |key, value|
|
|
481
|
+
value = configuration_value(value) if value.is_a?(Proc)
|
|
482
|
+
[key, Ractor.make_shareable(value)]
|
|
483
|
+
end.freeze
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def self.configuration_value(value)
|
|
487
|
+
case value
|
|
488
|
+
when Proc
|
|
489
|
+
return value if Ractor.shareable?(value)
|
|
490
|
+
|
|
491
|
+
# Like Rails callbacks, isolated callbacks use arguments and constants,
|
|
492
|
+
# not their initializer's implicit self. Keep lambda/proc arity intact.
|
|
493
|
+
value.lambda? ? Ractor.shareable_lambda(&value) : Ractor.shareable_proc(&value)
|
|
494
|
+
when Hash
|
|
495
|
+
value.to_h { |key, item| [configuration_value(key), configuration_value(item)] }.freeze
|
|
496
|
+
when Array
|
|
497
|
+
value.map { |item| configuration_value(item) }.freeze
|
|
498
|
+
else
|
|
499
|
+
Ractor.make_shareable(value, copy: true)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def self.configuration(original)
|
|
504
|
+
prepared = original.dup
|
|
505
|
+
original.instance_variables.each do |variable|
|
|
506
|
+
value = original.instance_variable_get(variable)
|
|
507
|
+
begin
|
|
508
|
+
shared = configuration_value(value)
|
|
509
|
+
prepared.instance_variable_set(variable, shared)
|
|
510
|
+
rescue Ractor::Error, TypeError => e
|
|
511
|
+
setting = variable.to_s.delete_prefix('@')
|
|
512
|
+
raise ArgumentError, "I18nProofreading.config.#{setting} cannot be shared with Ractors: #{e.message}"
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
Ractor.make_shareable(prepared)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
@@ -14,8 +14,8 @@ module I18nProofreading
|
|
|
14
14
|
# pipeline never ingests it: Rails auto-registers every engine's
|
|
15
15
|
# app/assets/* directory, which would put this file in the host's asset
|
|
16
16
|
# namespace as a bare "widget.js" and into its precompiled output.
|
|
17
|
-
SOURCE = File.expand_path('widget.js', __dir__)
|
|
18
|
-
DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__)
|
|
17
|
+
SOURCE = File.expand_path('widget.js', __dir__).freeze
|
|
18
|
+
DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__).freeze
|
|
19
19
|
|
|
20
20
|
# Right-to-left scripts, so the popover renders mirrored for those locales.
|
|
21
21
|
# Matched on the language subtag, so region variants ("ar-EG") count too.
|
data/lib/i18n_proofreading.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n_proofreading
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -86,6 +86,17 @@ files:
|
|
|
86
86
|
- config/locales/i18n_proofreading.vi.yml
|
|
87
87
|
- config/locales/i18n_proofreading.zh-CN.yml
|
|
88
88
|
- config/routes.rb
|
|
89
|
+
- docs/demo-640.mp4
|
|
90
|
+
- docs/demo.gif
|
|
91
|
+
- docs/ractors.md
|
|
92
|
+
- docs/screenshots/01-highlight.png
|
|
93
|
+
- docs/screenshots/02-suggest.png
|
|
94
|
+
- docs/screenshots/03-review-board.png
|
|
95
|
+
- docs/screenshots/admin-ui/after-index-mobile.png
|
|
96
|
+
- docs/screenshots/admin-ui/after-index.png
|
|
97
|
+
- docs/screenshots/admin-ui/after-show-mobile.png
|
|
98
|
+
- docs/screenshots/admin-ui/after-show.png
|
|
99
|
+
- docs/screenshots/admin-ui/before-index.png
|
|
89
100
|
- lib/generators/i18n_proofreading/install/install_generator.rb
|
|
90
101
|
- lib/generators/i18n_proofreading/install/templates/create_i18n_proofreading_suggestions.rb.tt
|
|
91
102
|
- lib/generators/i18n_proofreading/install/templates/initializer.rb
|
|
@@ -96,6 +107,7 @@ files:
|
|
|
96
107
|
- lib/i18n_proofreading/engine.rb
|
|
97
108
|
- lib/i18n_proofreading/marking.rb
|
|
98
109
|
- lib/i18n_proofreading/middleware.rb
|
|
110
|
+
- lib/i18n_proofreading/ractor_support.rb
|
|
99
111
|
- lib/i18n_proofreading/seeds.rb
|
|
100
112
|
- lib/i18n_proofreading/tag_helper.rb
|
|
101
113
|
- lib/i18n_proofreading/version.rb
|
|
@@ -124,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
124
136
|
- !ruby/object:Gem::Version
|
|
125
137
|
version: '0'
|
|
126
138
|
requirements: []
|
|
127
|
-
rubygems_version: 4.0.
|
|
139
|
+
rubygems_version: 4.0.20
|
|
128
140
|
specification_version: 4
|
|
129
141
|
summary: 'In-context i18n proofreading for Rails: click any translated string and
|
|
130
142
|
suggest a fix.'
|