Skip to content

Commit 5c9ce3a

Browse files
committed
feat: add multi-project support via for_project DSL with scoped rake tasks
1 parent 25e431d commit 5c9ce3a

9 files changed

Lines changed: 366 additions & 2 deletions

File tree

.rubocop.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Metrics/BlockLength:
2121
Exclude:
2222
- spec/**/*.rb
2323
- lokalise_rails.gemspec
24+
- lib/tasks/lokalise_rails_tasks.rake
2425

2526
Metrics/BlockNesting:
2627
Max: 2
@@ -80,4 +81,4 @@ Gemspec/DevelopmentDependencies:
8081

8182
RSpec/SpecFilePathFormat:
8283
Exclude:
83-
- spec/lib/generators/lokalise_rails/**/*.rb
84+
- spec/lib/generators/lokalise_rails/**/*.rb

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,58 @@ namespace :lokalise_custom do
127127
end
128128
```
129129

130+
## Multiple Lokalise projects
131+
132+
If your Rails app needs to sync with more than one Lokalise **project** (different `project_id`, possibly a different `api_token`), use `for_project` in `config/lokalise_rails.rb`. It registers a named, isolated config and automatically generates scoped rake tasks without changing the existing `lokalise_rails:import` and `lokalise_rails:export` tasks.
133+
134+
```ruby
135+
# config/lokalise_rails.rb
136+
137+
# Main project — unchanged
138+
LokaliseRails::GlobalConfig.config do |c|
139+
c.api_token = ENV['LOKALISE_API_TOKEN']
140+
c.project_id = ENV['LOKALISE_PROJECT_ID']
141+
end
142+
143+
# Additional project
144+
LokaliseRails::GlobalConfig.for_project(:mobile) do |c|
145+
c.project_id = ENV['LOKALISE_MOBILE_PROJECT_ID']
146+
c.locales_path = "#{Rails.root}/config/locales/mobile"
147+
end
148+
```
149+
150+
This generates the following rake tasks automatically:
151+
152+
```
153+
rake lokalise_rails:import # existing - uses the main config
154+
rake lokalise_rails:export # existing - uses the main config
155+
rake lokalise_rails:mobile:import # auto-generated — uses the :mobile config
156+
rake lokalise_rails:mobile:export # auto-generated — uses the :mobile config
157+
```
158+
159+
Any setting not explicitly set in the `for_project` block falls back to the main `GlobalConfig` - so shared options like `api_token` only need to be written once:
160+
161+
```ruby
162+
LokaliseRails::GlobalConfig.config do |c|
163+
c.api_token = ENV['LOKALISE_API_TOKEN'] # shared by all projects
164+
c.project_id = ENV['LOKALISE_PROJECT_ID']
165+
end
166+
167+
LokaliseRails::GlobalConfig.for_project(:mobile) do |c|
168+
# api_token is inherited from the main config above
169+
c.project_id = ENV['LOKALISE_MOBILE_PROJECT_ID']
170+
c.locales_path = "#{Rails.root}/config/locales/mobile"
171+
end
172+
```
173+
174+
You can register as many named projects as needed and sync them all in one command:
175+
176+
```
177+
rails lokalise_rails:import lokalise_rails:mobile:import
178+
```
179+
180+
> **Note:** `for_project` is designed for apps that need multiple distinct Lokalise **projects**. If you only need to sync multiple local directories against the same project, the `lokalise_custom` rake task pattern described above is sufficient.
181+
130182
## Configuration
131183

132184
Options are specified in the `config/lokalise_rails.rb` file.

lib/generators/templates/lokalise_rails_config.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,13 @@
6868
## Disable the import rake task:
6969
## c.disable_import_task = false
7070
end
71+
72+
# To sync with a second Lokalise project, register a named config.
73+
# This auto-generates lokalise_rails:<name>:import and lokalise_rails:<name>:export tasks.
74+
# Settings not set here fall back to the GlobalConfig block above.
75+
#
76+
# LokaliseRails::GlobalConfig.for_project(:mobile) do |c|
77+
# c.project_id = ENV['LOKALISE_MOBILE_PROJECT_ID']
78+
# c.locales_path = "#{Rails.root}/config/locales/mobile"
79+
# end
7180
end

lib/lokalise_rails/global_config.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,62 @@ def disable_import_task
3333
def locales_path
3434
@locales_path || "#{LokaliseRails::Utils.root}/config/locales"
3535
end
36+
37+
# Registers a named project config and auto-generates scoped rake tasks
38+
# (lokalise_rails:<name>:import and lokalise_rails:<name>:export).
39+
#
40+
# Settings not explicitly set in the block fall back to GlobalConfig
41+
# via LokaliseManager's inline override mechanism, so shared options
42+
# like api_token only need to be set once in the main config block.
43+
#
44+
# @param name [Symbol, String] identifier for the project
45+
# @yield [collector] block to configure project-specific settings
46+
#
47+
# @example
48+
# LokaliseRails::GlobalConfig.for_project(:mobile) do |c|
49+
# c.project_id = ENV['LOKALISE_MOBILE_PROJECT_ID']
50+
# c.locales_path = "#{Rails.root}/config/locales/mobile"
51+
# end
52+
def for_project(name, &block)
53+
collector = ProjectConfigCollector.new
54+
block&.call(collector)
55+
projects[name.to_sym] = collector.to_h
56+
end
57+
58+
# Returns the registry of named project configs.
59+
#
60+
# @return [Hash{Symbol => Hash}]
61+
def projects
62+
@projects ||= {}
63+
end
64+
end
65+
66+
# Collects attribute assignments from a for_project block into a plain hash.
67+
# The hash is passed as inline overrides to LokaliseManager.importer/exporter,
68+
# so any attribute not explicitly set falls back to GlobalConfig automatically.
69+
class ProjectConfigCollector
70+
def initialize
71+
@settings = {}
72+
end
73+
74+
def to_h
75+
@settings.dup
76+
end
77+
78+
def method_missing(name, *args)
79+
attr = name.to_s
80+
if attr.end_with?('=')
81+
@settings[attr.chomp('=').to_sym] = args.first
82+
else
83+
super
84+
end
85+
end
86+
87+
def respond_to_missing?(name, include_private = false)
88+
name.to_s.end_with?('=') || super
89+
end
3690
end
91+
92+
private_constant :ProjectConfigCollector
3793
end
3894
end

lib/tasks/lokalise_rails_tasks.rake

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,26 @@ namespace :lokalise_rails do
3737
rescue StandardError => e
3838
abort "Export failed: #{e.message}"
3939
end
40+
41+
# Auto-generate scoped import/export tasks for each named project
42+
# registered via LokaliseRails::GlobalConfig.for_project.
43+
LokaliseRails::GlobalConfig.projects.each do |project_name, project_opts|
44+
namespace project_name do
45+
desc "Import translations from Lokalise (#{project_name})"
46+
task :import do
47+
importer = LokaliseManager.importer(project_opts, LokaliseRails::GlobalConfig)
48+
importer.import!
49+
rescue StandardError => e
50+
abort "Import failed: #{e.message}"
51+
end
52+
53+
desc "Export translations to Lokalise (#{project_name})"
54+
task :export do
55+
exporter = LokaliseManager.exporter(project_opts, LokaliseRails::GlobalConfig)
56+
exporter.export!
57+
rescue StandardError => e
58+
abort "Export failed: #{e.message}"
59+
end
60+
end
61+
end
4062
end

spec/dummy/config/lokalise_rails.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@
55
c.api_token = ENV.fetch('LOKALISE_API_TOKEN', nil)
66
c.project_id = ENV.fetch('LOKALISE_PROJECT_ID', nil)
77
end
8+
9+
LokaliseRails::GlobalConfig.for_project(:dummy_project) do |c|
10+
c.project_id = ENV.fetch('LOKALISE_PROJECT_ID', nil)
11+
end
812
end

spec/lib/lokalise_rails/global_config_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,57 @@
159159
expect(fake_class).to have_received(:disable_import_task=)
160160
end
161161
end
162+
163+
describe '.for_project' do
164+
after { described_class.projects.clear }
165+
166+
it 'registers a named project with the given settings' do
167+
described_class.for_project(:mobile) do |c|
168+
c.project_id = 'mobile_id.123'
169+
c.locales_path = '/config/locales/mobile'
170+
end
171+
172+
expect(described_class.projects[:mobile]).to eq(
173+
project_id: 'mobile_id.123',
174+
locales_path: '/config/locales/mobile'
175+
)
176+
end
177+
178+
it 'supports registering multiple named projects' do
179+
described_class.for_project(:mobile) { |c| c.project_id = 'mobile.123' }
180+
described_class.for_project(:admin) { |c| c.project_id = 'admin.456' }
181+
182+
expect(described_class.projects.keys).to contain_exactly(:mobile, :admin)
183+
end
184+
185+
it 'stores an empty hash when no block is given' do
186+
described_class.for_project(:empty)
187+
188+
expect(described_class.projects[:empty]).to eq({})
189+
end
190+
191+
it 'accepts lambda values such as skip_file_export' do
192+
filter = ->(file) { file.include?('fr') }
193+
described_class.for_project(:mobile) { |c| c.skip_file_export = filter }
194+
195+
expect(described_class.projects[:mobile][:skip_file_export]).to eq(filter)
196+
end
197+
198+
it 'does not leak settings into GlobalConfig' do
199+
described_class.for_project(:mobile) { |c| c.project_id = 'mobile.123' }
200+
201+
expect(described_class.project_id).not_to eq('mobile.123')
202+
end
203+
204+
it 'raises NoMethodError when calling a getter (non-setter) in the block' do
205+
expect { described_class.for_project(:mobile, &:project_id) }.to raise_error(NoMethodError)
206+
end
207+
208+
it 'responds to setter methods but not to getter methods' do
209+
described_class.for_project(:mobile) do |c|
210+
expect(c.respond_to?(:project_id=)).to be true
211+
expect(c.respond_to?(:project_id)).to be false
212+
end
213+
end
214+
end
162215
end

0 commit comments

Comments
 (0)