purizumu 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41586286c24fbf46df721821901d6a0ef19c30cc9b28f4e186202140dd84f8da
4
- data.tar.gz: 9c1e474e116fa9f63c817f64f4e00727ee59e69d4c70577292e8a0446f194934
3
+ metadata.gz: 13e13e8ac2807b2872602bfc6066e18bb4b9c462c3abb09a4d9dc3922ffa93a8
4
+ data.tar.gz: 68f318bb8f0bfd86d86cf7e2f5490bcf1d47cc9c804ee8edb83d2c8c0f56b501
5
5
  SHA512:
6
- metadata.gz: 92e7c2d4c42e313b0381ab3f6e327f897dfac150d41644d1e60ffc2d7b5a99948dd5c5679d61b0a03a4d4b79d2e8eb3490104af6a4930ab58e8a700d99699695
7
- data.tar.gz: b7d7b134a4c85de69272411df15a21674188a9cedb3d811b9b58923e37d91774d0e7f27c0e97ff95a0c281a183e134631f380a44807381d0846ed165263f1e3c
6
+ metadata.gz: 818aaf90a7292954a157faa2787d17b1c9dfa643a865d23c7375c211d6a2f10a95e5fddf41f2c8e820b96c9abc1b6e98412873e183b96fe89bc9baf5da0154c2
7
+ data.tar.gz: 4a1784db93876921e7d32ea8f1db7ba9276017ce27f360aa081ce4aff4632f7e07d23f30424d36c7dc3db9489497ee3fda58b994ae7ccebc3e40c2b5473e2637
@@ -5,17 +5,40 @@ module Purizumu
5
5
  module ModelExtensions
6
6
  def self.included(base)
7
7
  base.extend(ClassMethods)
8
+ base.class_attribute :purizumu_translated_attributes, instance_accessor: false, default: []
8
9
  end
9
10
 
10
11
  # Defines translated attribute readers on Active Record models.
11
12
  module ClassMethods
12
13
  def attribute_translation(*attribute_names)
13
- attribute_names.flatten.each do |attribute_name|
14
+ translated_attribute_names = attribute_names.flatten.map(&:to_s)
15
+ self.purizumu_translated_attributes = (
16
+ purizumu_translated_attributes + translated_attribute_names
17
+ ).uniq
18
+
19
+ after_commit :purizumu_clear_translations_after_commit, on: :update
20
+
21
+ translated_attribute_names.each do |attribute_name|
14
22
  define_method(attribute_name) do
15
23
  Purizumu.translate_attribute(self, attribute_name)
16
24
  end
17
25
  end
18
26
  end
19
27
  end
28
+
29
+ private
30
+
31
+ def purizumu_clear_translations_after_commit
32
+ changed_translated_attributes = (
33
+ previous_changes.keys & self.class.purizumu_translated_attributes
34
+ )
35
+ return if changed_translated_attributes.empty?
36
+
37
+ Purizumu::Translation.where(
38
+ model_class_name: self.class.name,
39
+ record_id: id,
40
+ attribute_name: changed_translated_attributes
41
+ ).delete_all
42
+ end
20
43
  end
21
44
  end
@@ -11,6 +11,7 @@ module Purizumu
11
11
 
12
12
  def call
13
13
  return source_content if source_content.nil? || source_content == ''
14
+ return source_content unless record.persisted?
14
15
 
15
16
  existing_translation&.content || create_translation!.content
16
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Purizumu
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.2'
5
5
  end
@@ -9,11 +9,11 @@ RSpec.describe Purizumu::Translator do
9
9
  stub_const('GemRecord', Class.new(ActiveRecord::Base) do
10
10
  self.table_name = 'gems'
11
11
 
12
- attribute_translation :human_name
12
+ attribute_translation :human_name, :tagline
13
13
  end)
14
14
  end
15
15
 
16
- let!(:record) { GemRecord.create!(human_name: 'Prism') }
16
+ let!(:record) { GemRecord.create!(human_name: 'Prism', tagline: 'Crystal clear', slug: 'prism') }
17
17
  let(:translation_scope) do
18
18
  Purizumu::Translation.where(
19
19
  model_class_name: 'GemRecord',
@@ -109,12 +109,171 @@ RSpec.describe Purizumu::Translator do
109
109
 
110
110
  expect(Purizumu::Translation.where(record_id: blank_record.id)).to be_empty
111
111
  end
112
+
113
+ context 'with an unsaved record' do
114
+ let(:unsaved_record) { GemRecord.new(human_name: 'Prism', slug: 'prism') }
115
+
116
+ before do
117
+ allow(Purizumu).to receive(:engine)
118
+ I18n.locale = :jp
119
+ unsaved_record.human_name
120
+ end
121
+
122
+ it 'returns the source content' do
123
+ expect(unsaved_record.human_name).to eq('Prism')
124
+ end
125
+
126
+ it 'does not invoke the engine' do
127
+ expect(Purizumu).not_to have_received(:engine)
128
+ end
129
+
130
+ it 'does not create a translation row' do
131
+ expect(Purizumu::Translation.where(model_class_name: 'GemRecord')).to be_empty
132
+ end
133
+ end
134
+
135
+ context 'when validations read a translated attribute on create' do
136
+ before do
137
+ stub_validated_gem_record
138
+ allow(Purizumu).to receive(:engine)
139
+ I18n.locale = :jp
140
+ end
141
+
142
+ it 'does not raise during create' do
143
+ expect { create_validated_gem_record }.not_to raise_error
144
+ end
145
+
146
+ it 'does not invoke the engine' do
147
+ create_validated_gem_record
148
+
149
+ expect(Purizumu).not_to have_received(:engine)
150
+ end
151
+
152
+ it 'does not create translation rows' do
153
+ create_validated_gem_record
154
+
155
+ expect(Purizumu::Translation.where(model_class_name: 'ValidatedGemRecord')).to be_empty
156
+ end
157
+ end
158
+
159
+ context 'when callbacks read a translated attribute before validation on create' do
160
+ before do
161
+ stub_slugged_gem_record
162
+ allow(Purizumu).to receive(:engine)
163
+ I18n.locale = :jp
164
+ end
165
+
166
+ it 'allows the callback to derive values from the source attribute' do
167
+ expect(create_slugged_gem_record.slug).to eq('crystal-clear')
168
+ end
169
+
170
+ it 'does not invoke the engine' do
171
+ create_slugged_gem_record
172
+
173
+ expect(Purizumu).not_to have_received(:engine)
174
+ end
175
+
176
+ it 'does not create translation rows' do
177
+ create_slugged_gem_record
178
+
179
+ expect(Purizumu::Translation.where(model_class_name: 'SluggedGemRecord')).to be_empty
180
+ end
181
+ end
182
+
183
+ it 'clears persisted translations for a changed translated attribute after commit' do
184
+ create_existing_translation
185
+
186
+ expect { record.update!(human_name: 'Politics') }
187
+ .to change { translation_scope.pluck(:attribute_name, :locale, :content) }
188
+ .from([%w[human_name jp プリズム]]).to([])
189
+ end
190
+
191
+ it 'preserves other translated attribute rows when one translated attribute changes' do
192
+ create_existing_translation
193
+ create_tagline_translation
194
+ record.update!(human_name: 'Politics')
195
+
196
+ expect(tagline_translation_rows).to eq([%w[tagline jp とても明快]])
197
+ end
198
+
199
+ it 'regenerates translations from the new source content after invalidation' do
200
+ prepare_regenerated_translation
201
+
202
+ expect(record.human_name).to eq('Политика')
203
+ end
204
+
205
+ it 'persists regenerated translations after invalidation' do
206
+ prepare_regenerated_translation
207
+ record.human_name
208
+
209
+ expect(
210
+ translation_scope.find_by!(locale: 'ru').content
211
+ ).to eq('Политика')
212
+ end
213
+
214
+ it 'does not clear translations when only an untranslated attribute changes' do
215
+ create_existing_translation
216
+
217
+ expect do
218
+ record.update!(slug: 'renamed-prism')
219
+ end.not_to(change { translation_scope.pluck(:attribute_name, :locale, :content) })
220
+ end
112
221
  end
113
222
 
114
223
  def create_existing_translation
115
224
  Purizumu::Translation.create!(existing_translation_attributes)
116
225
  end
117
226
 
227
+ def stub_validated_gem_record
228
+ stub_const('ValidatedGemRecord', Class.new(ActiveRecord::Base) do
229
+ self.table_name = 'gems'
230
+
231
+ validates :human_name, presence: true
232
+ attribute_translation :human_name
233
+ end)
234
+ end
235
+
236
+ def create_validated_gem_record
237
+ ValidatedGemRecord.create!(human_name: 'Prism', slug: 'prism')
238
+ end
239
+
240
+ def stub_slugged_gem_record
241
+ stub_const('SluggedGemRecord', Class.new(ActiveRecord::Base) do
242
+ self.table_name = 'gems'
243
+
244
+ validates :human_name, presence: true
245
+ before_validation :generate_slug, if: -> { slug.blank? }
246
+ attribute_translation :human_name
247
+
248
+ def generate_slug
249
+ self.slug = human_name.to_s.parameterize
250
+ end
251
+ end)
252
+ end
253
+
254
+ def create_slugged_gem_record
255
+ SluggedGemRecord.create!(human_name: 'Crystal clear')
256
+ end
257
+
258
+ def prepare_regenerated_translation
259
+ create_existing_translation
260
+ allow(Purizumu).to receive(:engine).and_return(
261
+ instance_double(Purizumu::Engines::Xai, translate: 'Политика')
262
+ )
263
+ record.update!(human_name: 'Politics')
264
+ I18n.locale = :ru
265
+ end
266
+
267
+ def create_tagline_translation
268
+ Purizumu::Translation.create!(
269
+ model_class_name: 'GemRecord',
270
+ record_id: record.id,
271
+ attribute_name: 'tagline',
272
+ locale: 'jp',
273
+ content: 'とても明快'
274
+ )
275
+ end
276
+
118
277
  def expected_engine_arguments
119
278
  {
120
279
  model_name: 'GemRecord',
@@ -124,4 +283,12 @@ RSpec.describe Purizumu::Translator do
124
283
  source_locale: 'en'
125
284
  }
126
285
  end
286
+
287
+ def tagline_translation_rows
288
+ Purizumu::Translation.where(
289
+ model_class_name: 'GemRecord',
290
+ record_id: record.id,
291
+ attribute_name: 'tagline'
292
+ ).pluck(:attribute_name, :locale, :content)
293
+ end
127
294
  end
data/spec/spec_helper.rb CHANGED
@@ -24,6 +24,8 @@ RSpec.configure do |config|
24
24
  suppress_messages do
25
25
  create_table :gems, force: true do |t|
26
26
  t.string :human_name
27
+ t.string :slug
28
+ t.string :tagline
27
29
  end
28
30
 
29
31
  create_table :purizumu_translations, force: true do |t|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purizumu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenAI Codex
@@ -79,7 +79,7 @@ files:
79
79
  - spec/purizumu/generators/install_generator_spec.rb
80
80
  - spec/purizumu/translator_spec.rb
81
81
  - spec/spec_helper.rb
82
- - spec/tmp/generator/db/migrate/20260412193408_create_purizumu_translations.rb
82
+ - spec/tmp/generator/db/migrate/20260413083431_create_purizumu_translations.rb
83
83
  homepage: https://example.com/purizumu
84
84
  licenses:
85
85
  - MIT