embedded_localization 1.3.1 → 1.4.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.
@@ -0,0 +1,61 @@
1
+ module EmbeddedLocalization
2
+ module Storage
3
+ # ActiveRecord attribute type for a PostgreSQL hstore `i18n` column.
4
+ #
5
+ # hstore is a flat String => String map, so each translation is stored under the key "<locale>.<attribute>",
6
+ # e.g. "de.name" => "Science-Fiction". In Ruby the gem works with the nested Hash {de: {name: "Science-Fiction"}}.
7
+ # Reading and writing the hstore text format is delegated to ActiveRecord's own hstore type.
8
+ class Hstore < ::ActiveRecord::Type::Value
9
+ include ::ActiveModel::Type::Helpers::Mutable
10
+
11
+ SEPARATOR = '.'.freeze
12
+
13
+ def type
14
+ :hstore
15
+ end
16
+
17
+ def deserialize(value)
18
+ nest(hstore.deserialize(value))
19
+ end
20
+
21
+ def serialize(value)
22
+ hstore.serialize(flatten(value))
23
+ end
24
+
25
+ def changed_in_place?(raw_old_value, new_value)
26
+ deserialize(raw_old_value) != new_value
27
+ end
28
+
29
+ private
30
+
31
+ # ActiveRecord registers the hstore type when the PostgreSQL adapter is loaded, i.e. when the connection is
32
+ # established; it is looked up on first use, so a model can be defined before the connection exists.
33
+ def hstore
34
+ @hstore ||= ::ActiveRecord::Type.lookup(:hstore, adapter: :postgresql)
35
+ end
36
+
37
+ # {de: {name: "x"}} => {"de.name" => "x"}
38
+ def flatten(translations)
39
+ return translations unless translations.is_a?(::Hash)
40
+
41
+ translations.each_with_object({}) do |(locale, attributes), flat|
42
+ next unless attributes.is_a?(::Hash)
43
+
44
+ attributes.each { |attr_name, value| flat["#{locale}#{SEPARATOR}#{attr_name}"] = value }
45
+ end
46
+ end
47
+
48
+ # {"de.name" => "x"} => {de: {name: "x"}} ; keys without a separator are ignored
49
+ def nest(flat)
50
+ return flat unless flat.is_a?(::Hash)
51
+
52
+ flat.each_with_object({}) do |(key, value), translations|
53
+ locale, attr_name = key.split(SEPARATOR, 2)
54
+ next unless attr_name
55
+
56
+ (translations[locale.to_sym] ||= {})[attr_name.to_sym] = value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,25 @@
1
+ module EmbeddedLocalization
2
+ module Storage
3
+ # ActiveRecord attribute type for a json or jsonb `i18n` column.
4
+ #
5
+ # The column holds {"en" => {"name" => "..."}, "de" => {...}}; in Ruby the gem works with Symbol keys on both
6
+ # levels, so this type converts the keys whenever a value comes from the database. Assignment goes through the
7
+ # same conversion: ActiveRecord::Type::Json includes ActiveModel::Type::Helpers::Mutable, whose cast(value) is
8
+ # deserialize(serialize(value)).
9
+ class Json < ::ActiveRecord::Type::Json
10
+ def deserialize(value)
11
+ symbolize_keys(super)
12
+ end
13
+
14
+ private
15
+
16
+ def symbolize_keys(translations)
17
+ return translations unless translations.is_a?(::Hash)
18
+
19
+ translations.each_with_object({}) do |(locale, attributes), result|
20
+ result[locale.to_sym] = attributes.is_a?(::Hash) ? attributes.transform_keys(&:to_sym) : attributes
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module EmbeddedLocalization
2
- VERSION = '1.3.1'
2
+ VERSION = '1.4.0'
3
3
  end
@@ -1,12 +1,16 @@
1
1
  require "active_record"
2
2
  require "embedded_localization/version"
3
- require 'extensions/hash'
4
3
 
5
4
  module EmbeddedLocalization
6
5
  autoload :ActiveRecord, 'embedded_localization/active_record'
6
+
7
+ # ActiveRecord attribute types for the `i18n` column when it is not a YAML text column
8
+ module Storage
9
+ autoload :Json, 'embedded_localization/storage/json'
10
+ autoload :Hstore, 'embedded_localization/storage/hstore'
11
+ end
7
12
  end
8
13
 
9
- # we're assuming for now only to be used with ActiveRecord 3, which is auto-required above
10
14
  ActiveRecord::Base.extend(EmbeddedLocalization::ActiveRecord::ActMacro)
11
15
 
12
16
  #-
@@ -30,4 +34,3 @@ ActiveRecord::Base.extend(EmbeddedLocalization::ActiveRecord::ActMacro)
30
34
 
31
35
  # not as general:
32
36
  # I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
33
-
metadata CHANGED
@@ -1,17 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embedded_localization
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilo Sloboda
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: codecov
13
+ name: rspec
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
@@ -25,7 +24,7 @@ dependencies:
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: rspec
27
+ name: simplecov
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
@@ -82,18 +81,14 @@ dependencies:
82
81
  version: '0'
83
82
  description: 'Rails I18n: a very lightweight tool to allow you to transparently store
84
83
  multiple translations of attributes directly inside each DB record -- no extra database
85
- tables needed to store the localization data!'
84
+ tables needed to store the localization data! All translations of a record live
85
+ in one column: a YAML text column, a json/jsonb column, or a PostgreSQL hstore column.'
86
86
  email:
87
87
  - tilo.sloboda@gmail.com
88
88
  executables: []
89
89
  extensions: []
90
90
  extra_rdoc_files: []
91
91
  files:
92
- - ".github/dependabot.yml"
93
- - ".github/workflows/codeql-analysis.yml"
94
- - ".github/workflows/ruby.yml"
95
- - ".gitignore"
96
- - ".rspec"
97
92
  - CHANGELOG.md
98
93
  - Gemfile
99
94
  - README.md
@@ -104,14 +99,9 @@ files:
104
99
  - lib/embedded_localization/active_record/act_macro.rb
105
100
  - lib/embedded_localization/active_record/class_methods.rb
106
101
  - lib/embedded_localization/active_record/instance_methods.rb
102
+ - lib/embedded_localization/storage/hstore.rb
103
+ - lib/embedded_localization/storage/json.rb
107
104
  - lib/embedded_localization/version.rb
108
- - lib/extensions/hash.rb
109
- - spec/embedded_localization/native_column_spec.rb
110
- - spec/embedded_localization/simple_model_spec.rb
111
- - spec/models.rb
112
- - spec/schema.rb
113
- - spec/spec.opts
114
- - spec/spec_helper.rb
115
105
  homepage: https://github.com/tilo/embedded_localization
116
106
  licenses:
117
107
  - MIT
@@ -119,7 +109,7 @@ metadata:
119
109
  homepage_uri: https://github.com/tilo/embedded_localization
120
110
  source_code_uri: https://github.com/tilo/embedded_localization
121
111
  changelog_uri: https://github.com/tilo/embedded_localization/blob/main/CHANGELOG.md
122
- post_install_message:
112
+ bug_tracker_uri: https://github.com/tilo/embedded_localization/issues
123
113
  rdoc_options: []
124
114
  require_paths:
125
115
  - lib
@@ -134,14 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
124
  - !ruby/object:Gem::Version
135
125
  version: '0'
136
126
  requirements: []
137
- rubygems_version: 3.5.4
138
- signing_key:
127
+ rubygems_version: 4.0.17
139
128
  specification_version: 4
140
129
  summary: 'Rails I18n: library for embedded ActiveRecord model/data translation'
141
- test_files:
142
- - spec/embedded_localization/native_column_spec.rb
143
- - spec/embedded_localization/simple_model_spec.rb
144
- - spec/models.rb
145
- - spec/schema.rb
146
- - spec/spec.opts
147
- - spec/spec_helper.rb
130
+ test_files: []
@@ -1,8 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: "github-actions"
4
- directory: "/"
5
- schedule:
6
- interval: "weekly"
7
- day: "friday"
8
- time: "01:00" # UTC
@@ -1,72 +0,0 @@
1
- # For most projects, this workflow file will not need changing; you simply need
2
- # to commit it to your repository.
3
- #
4
- # You may wish to alter this file to override the set of languages analyzed,
5
- # or to provide custom queries or build logic.
6
- #
7
- # ******** NOTE ********
8
- # We have attempted to detect the languages in your repository. Please check
9
- # the `language` matrix defined below to confirm you have the correct set of
10
- # supported CodeQL languages.
11
- #
12
- name: "CodeQL"
13
-
14
- on:
15
- push:
16
- branches: [ "main" ]
17
- pull_request:
18
- # The branches below must be a subset of the branches above
19
- branches: [ "main" ]
20
- schedule:
21
- - cron: '39 11 * * 2'
22
-
23
- jobs:
24
- analyze:
25
- name: Analyze
26
- runs-on: ubuntu-latest
27
- permissions:
28
- actions: read
29
- contents: read
30
- security-events: write
31
-
32
- strategy:
33
- fail-fast: false
34
- matrix:
35
- language: [ 'ruby' ]
36
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38
-
39
- steps:
40
- - name: Checkout repository
41
- uses: actions/checkout@v4
42
-
43
- # Initializes the CodeQL tools for scanning.
44
- - name: Initialize CodeQL
45
- uses: github/codeql-action/init@v3
46
- with:
47
- languages: ${{ matrix.language }}
48
- # If you wish to specify custom queries, you can do so here or in a config file.
49
- # By default, queries listed here will override any specified in a config file.
50
- # Prefix the list here with "+" to use these queries and those in the config file.
51
-
52
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53
- # queries: security-extended,security-and-quality
54
-
55
-
56
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57
- # If this step fails, then you should remove it and run the build manually (see below)
58
- - name: Autobuild
59
- uses: github/codeql-action/autobuild@v3
60
-
61
- # ℹ️ Command-line programs to run using the OS shell.
62
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63
-
64
- # If the Autobuild fails above, remove it and uncomment the following three lines.
65
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66
-
67
- # - run: |
68
- # echo "Run, Build Application using script"
69
- # ./location_of_script_within_repo/buildscript.sh
70
-
71
- - name: Perform CodeQL Analysis
72
- uses: github/codeql-action/analyze@v3
@@ -1,64 +0,0 @@
1
- # install dependencies and run tests
2
- # https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
3
-
4
- name: Ruby
5
-
6
- on:
7
- push:
8
- branches:
9
- - main
10
-
11
- pull_request:
12
-
13
- jobs:
14
- test_matrix:
15
-
16
- strategy:
17
- fail-fast: false
18
- matrix:
19
- os:
20
- - ubuntu
21
- - macos
22
- ruby:
23
- - 2.5
24
- - 2.6
25
- - 2.7
26
- - "3.0"
27
- - 3.1
28
- - 3.2
29
- - 3.3
30
- - head
31
- - truffleruby
32
- - truffleruby-head
33
- - mingw
34
- - jruby
35
- - jruby-head
36
- exclude:
37
- - { os: ubuntu, ruby: mingw }
38
- - { os: macos, ruby: mingw }
39
- - { os: windows, ruby: truffleruby }
40
- - { os: windows, ruby: truffleruby-head }
41
-
42
- runs-on: ${{ matrix.os }}-latest
43
-
44
- steps:
45
- - name: clone ${{ github.repository }}
46
- uses: actions/checkout@v4
47
- - name: setup Ruby ${{ matrix.ruby }}
48
- uses: ruby/setup-ruby@v1
49
- with:
50
- ruby-version: ${{ matrix.ruby }}
51
- bundler-cache: true
52
- env:
53
- JAVA_OPTS: -Djdk.io.File.enableADS=true
54
- - name: Run tests
55
- run: bundle exec rake
56
- env:
57
- JAVA_OPTS: -Djdk.io.File.enableADS=true
58
-
59
- finish:
60
- runs-on: ubuntu-latest
61
- needs: [ test_matrix ]
62
- steps:
63
- - name: Wait for status checks
64
- run: echo "All Green!"
data/.gitignore DELETED
@@ -1,7 +0,0 @@
1
- #*#
2
- *~
3
- *.bak
4
- *.gem
5
- .bundle
6
- Gemfile.lock
7
- pkg/*
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper
@@ -1,7 +0,0 @@
1
- class Hash
2
- def self.zip(keys,values) # from Facets of Ruby library, CREDIT: Trans, Ara T. Howard
3
- h = {}
4
- keys.size.times{ |i| h[ keys[i] ] = values[i] }
5
- h
6
- end
7
- end
@@ -1,191 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
-
4
- I18n.locale = :en
5
-
6
- # e.g. the model "Movie" has :title as a translated field,
7
- # but also has :title as a native attribute for the model.
8
-
9
- describe 'model has translated field with attribute of that same name' do
10
- let(:movie){ Movie.new }
11
-
12
- describe "basic things that need to work" do
13
-
14
- it 'reports it translates' do
15
- Movie.translates?.should be_truthy
16
- end
17
-
18
- it 'correctly reports the list of translated_attributes' do
19
- Movie.translated_attributes.sort.should eq [:description, :title]
20
- end
21
-
22
- it 'correctly reports the list of translated_attribute_names' do
23
- Movie.translated_attribute_names.sort.should eq [:description, :title]
24
- end
25
-
26
- it 'correcty shows the translated attribute as translated' do
27
- Movie.translated?(:title).should be_truthy
28
- Movie.translated?(:description).should be_truthy
29
- end
30
-
31
- it 'correcty shows not translated attribute' do
32
- Movie.translated?(:other).should be_falsy
33
- end
34
- end
35
-
36
- describe "new DB records" do
37
-
38
- it 'correctly reports translated_locales for new record' do
39
- movie.translated_locales.should eq [I18n.default_locale]
40
- end
41
-
42
- it 'correctly reports translation_missing for new record' do
43
- movie.translation_missing.should be_truthy
44
- end
45
-
46
- it 'creates the accessor methods' do
47
- movie.methods.should include(:title)
48
- movie.methods.should include(:title=)
49
- movie.methods.should include(:description)
50
- movie.methods.should include(:description=)
51
- end
52
-
53
- it 'correctly reports translated field for new record for default locale' do
54
- movie.title.should be_nil
55
- movie.description.should be_nil
56
- movie.description( I18n.default_locale ).should be_nil
57
- end
58
-
59
- it 'correctly reports translated field for new record for other locale' do
60
- movie.title(:ko).should be_nil
61
- movie.description(:de).should be_nil
62
- end
63
-
64
- it 'correctly reports translation_coverage for new record' do
65
- movie.translation_coverage.should eq Hash.new
66
- end
67
-
68
- it 'correctly reports translation_goverate for attributes of new record' do
69
- movie.translation_coverage(:title).should eq []
70
- movie.translation_coverage(:description).should eq []
71
- end
72
-
73
- it 'translated_coverage returns nil for not-translated attributes' do
74
- movie.translation_coverage(:other).should be_nil
75
- end
76
-
77
- it 'correctly reports translation_missing for new record' do
78
- movie.translation_missing.should == {:description=>[:en], :title=>[:en]}
79
- movie.translation_missing(:title).should eq [:en]
80
- movie.translation_missing(:description).should eq [:en]
81
- end
82
- end
83
-
84
-
85
- describe "DB record with pre-set fields" do
86
- let(:title_en){ "Blade Runner" }
87
- let(:blade_runner){ Movie.new(:title => title_en) }
88
-
89
- it 'correctly shows the attribute for new record' do
90
- blade_runner.title.should eq title_en
91
- end
92
- end
93
-
94
-
95
- describe "updated DB record" do
96
- let(:title_en){ "Blade Runner" }
97
- let(:title_ru){'аЕЦСЫХИ ОН КЕГБХЧ'}
98
- let(:title_tr){"Ölüm takibi"}
99
- let(:title_de){"Der Blade Runner"}
100
- let(:blade_runner){ Movie.new(:title => title_en) }
101
-
102
- describe 'updating just default locale' do
103
-
104
- before :each do
105
- movie.title = title_en
106
- movie.description = "an awesome movie"
107
- movie.save
108
- movie.reload
109
- end
110
-
111
- it 'correctly reports the translated_locales' do
112
- movie.translated_locales.should eq [:en]
113
- end
114
-
115
- # when setting all fields in the default locale's languange:
116
- it 'correctly reports translation_missing for updated record' do
117
- movie.translation_missing.should eq Hash.new
118
- end
119
-
120
- it 'correctly reports translation_missing for attributes' do
121
- movie.translation_missing(:title).should eq nil
122
- movie.translation_missing(:description).should eq nil
123
- end
124
-
125
- it 'correctly reports translated_coverage for updated record' do
126
- movie.translation_coverage.should == {:title=>[:en], :description=>[:en]}
127
- end
128
-
129
- it 'correctly reports translated_coverage for attributes' do
130
- movie.translation_coverage(:title).should eq [:en]
131
- movie.translation_coverage(:description).should eq [:en]
132
- end
133
- end
134
-
135
- describe 'updating other locales' do
136
- before :each do
137
- movie.title = title_en
138
- movie.description = "an awesome movie"
139
- I18n.locale = :ru
140
- movie.title = title_ru
141
- movie.set_localized_attribute(:title , :tr, title_tr)
142
- I18n.locale = :de
143
- movie.description = "ein grossartiger Film"
144
- I18n.locale = I18n.default_locale # MAKE SURE you switch back to your default loale if you tweak it
145
- movie.save
146
- movie.reload
147
- end
148
-
149
- it 'correctly reports the translated_locales' do
150
- movie.translated_locales.should eq [:en, :ru, :tr, :de]
151
- end
152
-
153
- it 'can assign the translated field' do
154
- movie.title = title_en
155
- movie.save.should be_truthy
156
- movie.title.should eq title_en
157
- movie.title(:en).should eq title_en
158
- end
159
-
160
- # when setting all fields in additional languanges:
161
- it 'correctly reports values for updated record via attr(:locale)' do
162
- movie.title(:tr).should eq title_tr
163
- end
164
-
165
- it 'correctly reports values for updated record via getter' do
166
- movie.get_localized_attribute(:title, :ru).should eq title_ru
167
- end
168
-
169
- it 'correctly reports translation_coverage for updated record' do
170
- # what values are actually present
171
- movie.translation_coverage.should == {:title=>[:en, :ru, :tr], :description=>[:en, :de]}
172
- end
173
-
174
- it 'correctly reports translation_coverage for attributes' do
175
- movie.translation_coverage(:title).should eq [:en,:ru,:tr]
176
- movie.translation_coverage(:description).should eq [:en,:de]
177
- end
178
-
179
- it 'correctly reports translation_missing for updated record' do
180
- # what values are missing
181
- movie.translation_missing.should == {:description=>[:ru, :tr], :title=>[:de]}
182
- end
183
-
184
- it 'correctly reports translation_missing for attributes' do
185
- movie.translation_missing(:title).should eq [:de]
186
- movie.translation_missing(:description).should eq [:ru,:tr]
187
- end
188
- end
189
-
190
- end
191
- end