├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .github └── workflows │ ├── pull.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── awesome_hstore_translate.gemspec ├── bin ├── console └── setup ├── lib ├── awesome_hstore_translate.rb └── awesome_hstore_translate │ ├── active_record.rb │ ├── active_record │ ├── accessors.rb │ ├── act_as_translatable.rb │ ├── attributes.rb │ ├── class_methods.rb │ ├── core.rb │ ├── instance_methods.rb │ └── query_methods.rb │ └── version.rb └── test ├── awesome_hstore_translate_legacy_test.rb ├── awesome_hstore_translate_test.rb ├── database.yml ├── legacy_test_helper.rb ├── models ├── PageWithFallbacks.rb └── PageWithoutFallbacks.rb └── test_helper.rb /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster 2 | ARG VARIANT=2-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/ruby:0-${VARIANT} 4 | 5 | 6 | # [Optional] Uncomment this section to install additional OS packages. 7 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 8 | # && apt-get -y install --no-install-recommends 9 | 10 | # [Optional] Uncomment this line to install additional gems. 11 | # RUN gem install 12 | 13 | # [Optional] Uncomment this line to install global node packages. 14 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.202.5/containers/ruby-rails-postgres 3 | // Update the VARIANT arg in docker-compose.yml to pick a Ruby version 4 | { 5 | "name": "Awesome Hstore Translate", 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/workspace", 9 | 10 | // Set *default* container specific settings.json values on container create. 11 | "settings": { 12 | "sqltools.connections": [ 13 | { 14 | "name": "Test Database", 15 | "driver": "PostgreSQL", 16 | "previewLimit": 50, 17 | "server": "localhost", 18 | "port": 5432, 19 | 20 | // update this to match config/database.yml 21 | "database": "postgres", 22 | "username": "postgres" 23 | } 24 | ] 25 | }, 26 | 27 | // Add the IDs of extensions you want installed when the container is created. 28 | "extensions": [ 29 | "rebornix.Ruby", 30 | "mtxr.sqltools", 31 | "mtxr.sqltools-driver-pg" 32 | ], 33 | 34 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 35 | // "forwardPorts": [3000, 5432], 36 | 37 | // Use 'postCreateCommand' to run commands after the container is created. 38 | // "postCreateCommand": "bundle install && rake db:setup", 39 | 40 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 41 | //"remoteUser": "vscode" 42 | } -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | args: 9 | # Update 'VARIANT' to pick a version of Ruby: 3, 3.0, 2, 2.7, 2.6 10 | # Append -bullseye or -buster to pin to an OS version. 11 | # Use -bullseye variants on local arm64/Apple Silicon. 12 | VARIANT: "3" 13 | 14 | volumes: 15 | - ..:/workspace:cached 16 | 17 | # Overrides default command so things don't shut down after the process ends. 18 | command: sleep infinity 19 | 20 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 21 | network_mode: service:db 22 | # Uncomment the next line to use a non-root user for all processes. 23 | # user: vscode 24 | 25 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 26 | # (Adding the "ports" property to this file will not forward from a Codespace.) 27 | 28 | db: 29 | image: postgres:latest 30 | restart: unless-stopped 31 | volumes: 32 | - postgres-data:/var/lib/postgresql/data 33 | environment: 34 | POSTGRES_USER: postgres 35 | POSTGRES_DB: postgres 36 | POSTGRES_PASSWORD: postgres 37 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. 38 | # (Adding the "ports" property to this file will not forward from a Codespace.) 39 | 40 | volumes: 41 | postgres-data: null 42 | -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Check pull request 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | services: 10 | postgres: 11 | image: postgres:14-alpine 12 | env: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: postgres 15 | POSTGRES_DB: postgres 16 | options: >- 17 | --health-cmd pg_isready 18 | --health-interval 10s 19 | --health-timeout 5s 20 | --health-retries 5 21 | ports: 22 | - 5432:5432 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: 3.0 28 | bundler-cache: true 29 | - name: Install PostgreSQL client 30 | run: sudo apt-get -yqq install libpq-dev 31 | - name: Run tests 32 | run: bundle exec rake -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to RubyGems 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 3.0 # Not needed with a .ruby-version file 16 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 17 | - name: publish gem 18 | run: | 19 | mkdir -p $HOME/.gem 20 | touch $HOME/.gem/credentials 21 | chmod 0600 $HOME/.gem/credentials 22 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 23 | gem build *.gemspec 24 | gem push *.gem 25 | env: 26 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_TOKEN}}" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/ruby,visualstudiocode,windows,linux,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=ruby,visualstudiocode,windows,linux,macos 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Ruby ### 50 | *.gem 51 | *.rbc 52 | /.config 53 | /coverage/ 54 | /InstalledFiles 55 | /pkg/ 56 | /spec/reports/ 57 | /spec/examples.txt 58 | /test/tmp/ 59 | /test/version_tmp/ 60 | /tmp/ 61 | 62 | # Used by dotenv library to load environment variables. 63 | # .env 64 | 65 | # Ignore Byebug command history file. 66 | .byebug_history 67 | 68 | ## Specific to RubyMotion: 69 | .dat* 70 | .repl_history 71 | build/ 72 | *.bridgesupport 73 | build-iPhoneOS/ 74 | build-iPhoneSimulator/ 75 | 76 | ## Specific to RubyMotion (use of CocoaPods): 77 | # 78 | # We recommend against adding the Pods directory to your .gitignore. However 79 | # you should judge for yourself, the pros and cons are mentioned at: 80 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 81 | # vendor/Pods/ 82 | 83 | ## Documentation cache and generated files: 84 | /.yardoc/ 85 | /_yardoc/ 86 | /doc/ 87 | /rdoc/ 88 | 89 | ## Environment normalization: 90 | /.bundle/ 91 | /vendor/bundle 92 | /lib/bundler/man/ 93 | 94 | # for a library or gem, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # Gemfile.lock 97 | # .ruby-version 98 | # .ruby-gemset 99 | 100 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 101 | .rvmrc 102 | 103 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 104 | # .rubocop-https?--* 105 | 106 | ### VisualStudioCode ### 107 | .vscode/* 108 | !.vscode/settings.json 109 | !.vscode/tasks.json 110 | !.vscode/launch.json 111 | !.vscode/extensions.json 112 | *.code-workspace 113 | 114 | # Local History for Visual Studio Code 115 | .history/ 116 | 117 | ### VisualStudioCode Patch ### 118 | # Ignore all local history of files 119 | .history 120 | .ionide 121 | 122 | ### Windows ### 123 | # Windows thumbnail cache files 124 | Thumbs.db 125 | Thumbs.db:encryptable 126 | ehthumbs.db 127 | ehthumbs_vista.db 128 | 129 | # Dump file 130 | *.stackdump 131 | 132 | # Folder config file 133 | [Dd]esktop.ini 134 | 135 | # Recycle Bin used on file shares 136 | $RECYCLE.BIN/ 137 | 138 | # Windows Installer files 139 | *.cab 140 | *.msi 141 | *.msix 142 | *.msm 143 | *.msp 144 | 145 | # Windows shortcuts 146 | *.lnk 147 | 148 | # End of https://www.toptal.com/developers/gitignore/api/ruby,visualstudiocode,windows,linux,macos 149 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.3.0](https://www.github.com/openscript/awesome_hstore_translate/compare/v0.2.2...v0.3.0) (2021-10-25) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Adds compatibility with Rails 6 9 | 10 | ### Features 11 | 12 | * Adds compatibility with Rails 6 ([3fdd9bc](https://www.github.com/openscript/awesome_hstore_translate/commit/3fdd9bc51e770fae74c867b6d28c182623394ab0)) 13 | * Updates ([5c7b7e7](https://www.github.com/openscript/awesome_hstore_translate/commit/5c7b7e7883bb87452ef60a8c629fb45146381a20)) 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at nospam@example.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | awesome_hstore_translate (0.4.0) 5 | activemodel (>= 5.0) 6 | activerecord (>= 5.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activemodel (6.1.4.1) 12 | activesupport (= 6.1.4.1) 13 | activerecord (6.1.4.1) 14 | activemodel (= 6.1.4.1) 15 | activesupport (= 6.1.4.1) 16 | activesupport (6.1.4.1) 17 | concurrent-ruby (~> 1.0, >= 1.0.2) 18 | i18n (>= 1.6, < 2) 19 | minitest (>= 5.1) 20 | tzinfo (~> 2.0) 21 | zeitwerk (~> 2.3) 22 | byebug (11.1.3) 23 | concurrent-ruby (1.1.9) 24 | database_cleaner (2.0.1) 25 | database_cleaner-active_record (~> 2.0.0) 26 | database_cleaner-active_record (2.0.1) 27 | activerecord (>= 5.a) 28 | database_cleaner-core (~> 2.0.0) 29 | database_cleaner-core (2.0.1) 30 | docile (1.4.0) 31 | i18n (1.8.10) 32 | concurrent-ruby (~> 1.0) 33 | minitest (5.14.4) 34 | pg (1.2.3) 35 | rake (13.0.6) 36 | simplecov (0.21.2) 37 | docile (~> 1.1) 38 | simplecov-html (~> 0.11) 39 | simplecov_json_formatter (~> 0.1) 40 | simplecov-html (0.12.3) 41 | simplecov_json_formatter (0.1.3) 42 | tzinfo (2.0.4) 43 | concurrent-ruby (~> 1.0) 44 | zeitwerk (2.5.1) 45 | 46 | PLATFORMS 47 | x86_64-linux 48 | 49 | DEPENDENCIES 50 | awesome_hstore_translate! 51 | bundler (~> 2.2) 52 | byebug (~> 11.1) 53 | database_cleaner (~> 2.0) 54 | minitest (~> 5.0) 55 | pg (~> 1.2) 56 | rake (~> 13.0) 57 | simplecov (~> 0.21) 58 | 59 | BUNDLED WITH 60 | 2.2.22 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Robin Bühler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awesome Hstore Translate 2 | 3 | [![Gem Version](https://badge.fury.io/rb/awesome_hstore_translate.svg)](https://badge.fury.io/rb/awesome_hstore_translate) 4 | 5 | This gem uses PostgreSQLs hstore datatype and ActiveRecord models to translate model data. It is based on the gem 6 | [`hstore_translate`](https://github.com/Leadformance/hstore_translate) by Rob Worely. An alternative is [`json_translate`](https://github.com/cfabianski/json_translate). 7 | 8 | - Works with Rails 5 and 6 9 | - No extra columns or tables needed to operate 10 | - Clean naming in the database model 11 | - Everything is well tested 12 | 13 | ## Features 14 | 15 | - [x] `v0.1.0` Attributes override / Raw attributes 16 | - [x] `v0.1.0` Fallbacks 17 | - [x] `v0.1.0` Language specific accessors 18 | - [x] `v0.2.0` Awesome Hstore Translate as drop in replace for [`hstore_translate`](https://github.com/Leadformance/hstore_translate) 19 | - `with_[attr]_translation(str)` is not supported 20 | - [x] `v0.2.2` Support record selection via ActiveRecord (e. g. `where`, `find_by`, ..) 21 | - [x] `v0.3.0` Support record ordering via ActiveRecord `order` 22 | - [ ] `backlog` Support `friendly_id` (see `friendly_id-awesome_hstore` gem) 23 | 24 | ## Requirements 25 | 26 | - ActiveRecord `>= 5` 27 | - Please use [`hstore_translate`](https://github.com/Leadformance/hstore_translate), if you are on an older version. 28 | - I18n 29 | 30 | ## Installation 31 | 32 | Add this line to your application's Gemfile: 33 | 34 | ```ruby 35 | gem 'awesome_hstore_translate' 36 | ``` 37 | 38 | And then execute: 39 | 40 | $ bundle 41 | 42 | Or install it yourself as: 43 | 44 | $ gem install awesome_hstore_translate 45 | 46 | ## Usage 47 | 48 | Use `translates` in your models, to define the attributes, which should be translateable: 49 | 50 | ```ruby 51 | class Page < ActiveRecord::Base 52 | translates :title, :content 53 | end 54 | ``` 55 | 56 | Make sure that the datatype of this columns is `hstore`: 57 | 58 | ```ruby 59 | class CreatePages < ActiveRecord::Migration 60 | def change 61 | # Make sure you enable the hstore extenion 62 | enable_extension 'hstore' unless extension_enabled?('hstore') 63 | 64 | create_table :pages do |t| 65 | t.column :title, :hstore 66 | t.column :content, :hstore 67 | t.timestamps 68 | end 69 | end 70 | end 71 | ``` 72 | 73 | Use the model attributes per locale: 74 | 75 | ```ruby 76 | p = Page.first 77 | 78 | I18n.locale = :en 79 | p.title # => English title 80 | 81 | I18n.locale = :de 82 | p.title # => Deutscher Titel 83 | 84 | I18n.with_locale :en do 85 | p.title # => English title 86 | end 87 | ``` 88 | 89 | The raw data is available via the suffix `_raw`: 90 | 91 | ```ruby 92 | p = Page.new(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 93 | 94 | p.title_raw # => {'en' => 'English title', 'de' => 'Deutscher Titel'} 95 | ``` 96 | 97 | Translated attributes: 98 | 99 | ```ruby 100 | Page.translated_attribute_names # [:title] 101 | ``` 102 | 103 | ### Fallbacks 104 | 105 | It's possible to fall back to another language, if there is no or an empty value for the primary language. To enable fallbacks you can set `I18n.fallbacks` to `true` or enable it manually in the model: 106 | 107 | ```ruby 108 | class Page < ActiveRecord::Base 109 | translates :title, :content, fallbacks: true 110 | end 111 | ``` 112 | 113 | Set `I18n.default_locale` or `I18n.fallbacks` to define the fallback: 114 | 115 | ```ruby 116 | I18n.fallbacks.map(:en => :de) # => if :en is nil or empty, it will use :de 117 | 118 | p = Page.new(:title_raw => {'de' => 'Deutscher Titel'}) 119 | 120 | I18n.with_locale :en do 121 | p.title # => Deutscher Titel 122 | end 123 | ``` 124 | 125 | It's possible to activate (`with_fallbacks`) or deactivate (`without_fallbacks`) fallbacks for a block execution: 126 | 127 | ```ruby 128 | p = PageWithoutFallbacks.new(:title_raw => {'de' => 'Deutscher Titel'}) 129 | 130 | I18n.with_locale(:en) do 131 | PageWithoutFallbacks.with_fallbacks do 132 | assert_equal('Deutscher Titel', p.title) 133 | end 134 | end 135 | ``` 136 | 137 | ### Accessors 138 | 139 | Convenience accessors can be enabled via the model descriptor: 140 | 141 | ```ruby 142 | class Page < ActiveRecord::Base 143 | translates :title, :content, accessors: [:de, :en] 144 | end 145 | ``` 146 | 147 | It's also make sense to activate the accessors for all available locales: 148 | 149 | ```ruby 150 | class Page < ActiveRecord::Base 151 | translates :title, :content, accessors: I18n.available_locales 152 | end 153 | ``` 154 | 155 | Now locale-suffixed accessors can be used: 156 | 157 | ```ruby 158 | p = Page.create!(:title_en => 'English title', :title_de => 'Deutscher Titel') 159 | 160 | p.title_en # => English title 161 | p.title_de # => Deutscher Titel 162 | ``` 163 | 164 | Translated accessor attributes: 165 | 166 | ```ruby 167 | Page.translated_accessor_names # [:title_en, :title_de] 168 | ``` 169 | 170 | ### Find 171 | 172 | `awesome_hstore_translate` patches ActiveRecord, so you can conviniently use `where` and `find_by` as you like. 173 | 174 | ```ruby 175 | Page.create!(:title_en => 'English title', :title_de => 'Deutscher Titel') 176 | Page.create!(:title_en => 'Another English title', :title_de => 'Noch ein Deutscher Titel') 177 | 178 | Page.where(title: 'Another English title') # => Page with title 'Another English title' 179 | ``` 180 | 181 | ### Order 182 | 183 | `awesome_hstore_translate` patches ActiveRecord, so you can conviniently use `order` as you like. 184 | 185 | ```ruby 186 | Page.create!(:title_en => 'English title', :title_de => 'Deutscher Titel') 187 | Page.create!(:title_en => 'Another English title', :title_de => 'Noch ein Deutscher Titel') 188 | 189 | Page.all.order(title: :desc) # => Page with title 'English title' 190 | ``` 191 | 192 | ### Limitations 193 | 194 | `awesome_hstore_translate` patches ActiveRecord, which create the limitation, that a with `where` chained `first_or_create` and `first_or_create!` **doesn't work** as expected. 195 | Here is an example, which **won't** work: 196 | 197 | ```ruby 198 | Page.where(title: 'Titre français').first_or_create! 199 | ``` 200 | 201 | A workaround is: 202 | 203 | ```ruby 204 | Page.where(title: 'Titre français').first_or_create!(title: 'Titre français') 205 | ``` 206 | 207 | The where clause is internally rewritten to `WHERE 'Titre français' = any(avals(title))`, so the `title: 'Titre français'` is not bound to the scope. 208 | 209 | ### Upgrade from [`hstore_translate`](https://github.com/Leadformance/hstore_translate) 210 | 211 | 1. Replace the [`hstore_translate`](https://github.com/Leadformance/hstore_translate) with `awesome_hstore_translate` in your Gemfile 212 | 1. Activate accessors, if you used the [`hstore_translate`](https://github.com/Leadformance/hstore_translate) accessors 213 | 1. Replace `with_[attr]_translation(str)` with equivalents (see "Support record selection via ActiveRecord" feature) 214 | 215 | ## Development 216 | 217 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 218 | 219 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 220 | 221 | ## Contributing 222 | 223 | Bug reports and pull requests are welcome on [GitHub](https://github.com/openscript/awesome_hstore_translate). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 224 | 225 | ## License 226 | 227 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 228 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << 'test' 6 | t.libs << 'lib' 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /awesome_hstore_translate.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'awesome_hstore_translate/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'awesome_hstore_translate' 8 | spec.version = AwesomeHstoreTranslate::VERSION 9 | spec.authors = ['Robin Bühler', 'Rob Worley', 'Edouard Piron'] 10 | spec.email = ['r+rubygems@obin.ch'] 11 | 12 | spec.summary = 'Using PostgreSQLs hstore datatype to provide ActiveRecord models data translation.' 13 | spec.description = 'This gem uses PostgreSQLs hstore datatype and ActiveRecord models to translate model data. It is based on the gem hstore_translate by Rob Worely.' 14 | spec.homepage = 'https://github.com/openscript/awesome_hstore_translate' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|\.idea)/}) } 18 | spec.bindir = 'bin' 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'activerecord', '>= 5.0' 23 | spec.add_dependency 'activemodel', '>= 5.0' 24 | 25 | spec.add_development_dependency 'byebug', '~> 11.1' 26 | spec.add_development_dependency 'bundler', '~> 2.2' 27 | spec.add_development_dependency 'rake', '~> 13.0' 28 | spec.add_development_dependency 'minitest', '~> 5.0' 29 | spec.add_development_dependency 'database_cleaner', '~> 2.0' 30 | spec.add_development_dependency 'pg', '~> 1.2' 31 | spec.add_development_dependency 'simplecov', '~> 0.21' 32 | end 33 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "awesome_hstore_translate" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/awesome_hstore_translate.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'awesome_hstore_translate/version' 3 | 4 | module AwesomeHstoreTranslate 5 | autoload :ActiveRecord, 'awesome_hstore_translate/active_record' 6 | end 7 | 8 | ActiveRecord::Base.extend(AwesomeHstoreTranslate::ActiveRecord::ActAsTranslatable) 9 | -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | autoload :Accessors, 'awesome_hstore_translate/active_record/accessors' 4 | autoload :ActAsTranslatable, 'awesome_hstore_translate/active_record/act_as_translatable' 5 | autoload :Attributes, 'awesome_hstore_translate/active_record/attributes' 6 | autoload :ClassMethods, 'awesome_hstore_translate/active_record/class_methods' 7 | autoload :Core, 'awesome_hstore_translate/active_record/core' 8 | autoload :InstanceMethods, 'awesome_hstore_translate/active_record/instance_methods' 9 | autoload :QueryMethods, 'awesome_hstore_translate/active_record/query_methods' 10 | end 11 | end -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/accessors.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module Accessors 4 | protected 5 | 6 | def define_accessors(attr) 7 | translation_options[:accessors].each do |locale| 8 | define_reader_accessor(attr, locale) 9 | define_writer_accessor(attr, locale) 10 | self.translated_accessor_names << :"#{attr}_#{locale}" 11 | end 12 | end 13 | 14 | def define_reader_accessor(attr, locale) 15 | define_method get_accessor_name(attr, locale) do 16 | read_translated_attribute(attr, locale) 17 | end 18 | end 19 | 20 | def define_writer_accessor(attr, locale) 21 | define_method "#{get_accessor_name(attr, locale)}=" do |value| 22 | write_translated_attribute(attr, value, locale) 23 | end 24 | end 25 | 26 | def get_accessor_name(attr, locale) 27 | "#{attr}_#{locale.to_s.underscore}" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/act_as_translatable.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module ActAsTranslatable 4 | def translates(*attr_names) 5 | options = attr_names.extract_options! 6 | 7 | bootstrap(options, attr_names) 8 | 9 | if attr_names.present? 10 | enable_attributes(attr_names) 11 | enable_accessors(attr_names) if options[:accessors] 12 | end 13 | end 14 | 15 | protected 16 | 17 | def enable_attributes(attr_names) 18 | extend Attributes 19 | attr_names.each do |attr_name| 20 | define_attributes(attr_name) 21 | end 22 | end 23 | 24 | def enable_accessors(attr_names) 25 | extend Accessors 26 | attr_names.each do |attr_name| 27 | define_accessors(attr_name) 28 | end 29 | end 30 | 31 | def apply_options(options) 32 | fallbacks = I18n.respond_to?(:fallbacks) ? I18n.fallbacks : true 33 | options[:fallbacks] = fallbacks unless options.include?(:fallbacks) 34 | options[:accessors] = false unless options.include?(:accessors) 35 | 36 | class_attribute :translation_options 37 | self.translation_options = options 38 | end 39 | 40 | def expose_translated_attrs(attr_names) 41 | class_attribute :translated_attribute_names 42 | self.translated_attribute_names = attr_names 43 | 44 | class_attribute :translated_accessor_names 45 | self.translated_accessor_names = [] 46 | end 47 | 48 | def bootstrap(options, attr_names) 49 | apply_options(options) 50 | expose_translated_attrs(attr_names) 51 | 52 | include InstanceMethods 53 | extend Core::ClassMethods 54 | extend ClassMethods 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/attributes.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module Attributes 4 | protected 5 | 6 | def define_attributes(attr) 7 | define_reader_attribute(attr) 8 | define_writer_attribute(attr) 9 | define_raw_reader_attribute(attr) 10 | define_raw_writer_attribute(attr) 11 | end 12 | 13 | def define_reader_attribute(attr) 14 | define_method(attr) do 15 | read_translated_attribute(attr) 16 | end 17 | end 18 | 19 | def define_raw_reader_attribute(attr) 20 | define_method(:"#{attr}_raw") do 21 | read_raw_attribute(attr) 22 | end 23 | end 24 | 25 | def define_writer_attribute(attr) 26 | define_method(:"#{attr}=") do |value| 27 | write_translated_attribute(attr, value) 28 | end 29 | end 30 | 31 | def define_raw_writer_attribute(attr) 32 | define_method(:"#{attr}_raw=") do |value| 33 | write_raw_attribute(attr, value) 34 | end 35 | end 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/class_methods.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module ClassMethods 4 | def translates? 5 | included_modules.include?(InstanceMethods) 6 | end 7 | 8 | def without_fallbacks(&block) 9 | before_state = translation_options[:fallbacks] 10 | toggle_fallback if translation_options[:fallbacks] 11 | yield block 12 | translation_options[:fallbacks] = before_state 13 | end 14 | 15 | def with_fallbacks(&block) 16 | before_state = translation_options[:fallbacks] 17 | toggle_fallback unless translation_options[:fallbacks] 18 | yield block 19 | translation_options[:fallbacks] = before_state 20 | end 21 | 22 | protected 23 | 24 | def toggle_fallback 25 | translation_options[:fallbacks] = !translation_options[:fallbacks] 26 | end 27 | 28 | private 29 | 30 | # Override the default relation methods in order to inject custom finder methods for hstore translations. 31 | def relation 32 | super.extending!(QueryMethods) 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/core.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module Core 4 | module ClassMethods 5 | def find_by(*args) 6 | attrs = args.first 7 | if attrs.is_a?(Hash) && contains_translated_attributes(attrs) 8 | where(attrs).limit(1).first 9 | else 10 | super 11 | end 12 | end 13 | 14 | private 15 | 16 | def contains_translated_attributes(attrs) 17 | !(self.translated_attribute_names & attrs.keys).empty? 18 | end 19 | end 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/instance_methods.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module InstanceMethods 4 | protected 5 | 6 | def read_translated_attribute(attr, locale = I18n.locale) 7 | locales = Array(locale) 8 | locales += get_fallback_for_locale(locale) || [] if translation_options[:fallbacks] 9 | 10 | translations = read_raw_attribute(attr) 11 | 12 | if translations 13 | locales.map(&:to_s).uniq.each do |cur| 14 | if translations.has_key?(cur) && !translations[cur].blank? 15 | return translations[cur] 16 | end 17 | end 18 | end 19 | 20 | nil 21 | end 22 | 23 | def read_raw_attribute(attr) 24 | read_attribute(get_column_name(attr)) 25 | end 26 | 27 | def write_translated_attribute(attr, value, locale= I18n.locale) 28 | translations = read_raw_attribute(attr) || {} 29 | translations[locale] = value 30 | write_raw_attribute(attr, translations) 31 | end 32 | 33 | def write_raw_attribute(attr, value) 34 | write_attribute(get_column_name(attr), value) 35 | end 36 | 37 | def get_fallback_for_locale(locale) 38 | I18n.fallbacks[locale] if I18n.respond_to?(:fallbacks) 39 | end 40 | 41 | private 42 | 43 | def get_column_name(attr) 44 | column_name = attr.to_s 45 | # detect column from original hstore_translate 46 | column_name << '_translations' if !has_attribute?(column_name) && has_attribute?("#{column_name}_translations") 47 | 48 | column_name 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/active_record/query_methods.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | module ActiveRecord 3 | module QueryMethods 4 | def where(opts = :chain, *rest) 5 | if opts.is_a?(Hash) 6 | query = spawn 7 | translated_attrs = translated_attributes(opts) 8 | untranslated_attrs = untranslated_attributes(opts) 9 | 10 | unless untranslated_attrs.empty? 11 | query.where!(untranslated_attrs, *rest) 12 | end 13 | 14 | translated_attrs.each do |key, value| 15 | if value.is_a?(String) 16 | query.where!(":value = any(avals(#{key}))", value: value) 17 | else 18 | super 19 | end 20 | end 21 | 22 | query 23 | else 24 | super 25 | end 26 | end 27 | 28 | def order(*args) 29 | if args.is_a?(Array) 30 | check_if_method_has_arguments!(:order, args) 31 | query = spawn 32 | attrs = args 33 | 34 | # TODO Remove this ugly hack 35 | if args[0].is_a?(Hash) 36 | attrs = args[0] 37 | elsif args[0].is_a?(Symbol) 38 | attrs = Hash[args.map {|attr| [attr, :asc]}] 39 | end 40 | 41 | translated_attrs = translated_attributes(attrs) 42 | untranslated_attrs = untranslated_attributes(attrs) 43 | 44 | unless untranslated_attrs.empty? 45 | query.order!(untranslated_attrs) 46 | end 47 | 48 | translated_attrs.each do |key, value| 49 | query.order!(Arel.sql("#{key} -> '#{I18n.locale.to_s}' #{value}")) 50 | end 51 | 52 | query 53 | else 54 | super 55 | end 56 | end 57 | 58 | private 59 | 60 | def translated_attributes(opts) 61 | opts.select{ |key, _| self.translated_attribute_names.include?(key) } 62 | end 63 | 64 | def untranslated_attributes(opts) 65 | return safe_untranslated_attributes(opts) if opts.is_a?(Array) 66 | 67 | opts.reject{ |key, _| self.translated_attribute_names.include?(key) } 68 | end 69 | 70 | def safe_untranslated_attributes(opts) 71 | opts 72 | .reject { |opt| opt.is_a?(Arel::Nodes::Ordering) } 73 | .map! { |opt| Arel.sql(opt.to_s) } 74 | end 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /lib/awesome_hstore_translate/version.rb: -------------------------------------------------------------------------------- 1 | module AwesomeHstoreTranslate 2 | VERSION = '0.4.0' 3 | end 4 | -------------------------------------------------------------------------------- /test/awesome_hstore_translate_legacy_test.rb: -------------------------------------------------------------------------------- 1 | require 'legacy_test_helper' 2 | 3 | class AwesomeHstoreTranslateLegacyTest < AwesomeHstoreTranslate::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil AwesomeHstoreTranslate::VERSION 6 | end 7 | 8 | def test_translated_attributes 9 | attr_names = PageWithFallbacks.translated_attribute_names 10 | assert_equal([:title], attr_names) 11 | end 12 | 13 | def test_translated_accessors 14 | attr_names = PageWithFallbacks.translated_accessor_names 15 | assert_equal([:title_en, :title_de], attr_names) 16 | end 17 | 18 | def test_assigns_in_current_locale 19 | I18n.with_locale(:en) do 20 | p = PageWithoutFallbacks.new(:title => 'English title') 21 | assert_equal('English title', p.title_raw['en']) 22 | end 23 | end 24 | 25 | def test_retrieves_in_current_locale 26 | p = PageWithoutFallbacks.new(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 27 | I18n.with_locale(:de) do 28 | assert_equal('Deutscher Titel', p.title) 29 | end 30 | end 31 | 32 | def test_retrieves_in_current_locale_with_fallbacks 33 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 34 | I18n.fallbacks = [:'en-US'] 35 | 36 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 37 | I18n.with_locale(:de) do 38 | assert_equal('English title', p.title) 39 | end 40 | end 41 | 42 | def test_retrieves_in_current_locale_without_fallbacks 43 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 44 | I18n.fallbacks = [:'en'] 45 | 46 | p = PageWithoutFallbacks.new(:title_raw => {'en' => 'English title'}) 47 | I18n.with_locale(:de) do 48 | assert_nil(p.title) 49 | end 50 | end 51 | 52 | 53 | def test_retrieves_with_regional_enabled_fallbacks 54 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 55 | I18n.fallbacks[:en] = [:de] 56 | 57 | p = PageWithoutFallbacks.new(:title_raw => {'de' => 'Deutscher Titel'}) 58 | I18n.with_locale(:en) do 59 | PageWithoutFallbacks.with_fallbacks do 60 | assert_equal('Deutscher Titel', p.title) 61 | end 62 | end 63 | end 64 | 65 | 66 | def test_retrieves_with_regional_disabled_fallbacks 67 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 68 | I18n.fallbacks[:en] = [:de] 69 | 70 | p = PageWithFallbacks.new(:title_raw => {'de' => 'Deutscher Titel'}) 71 | I18n.with_locale(:en) do 72 | PageWithFallbacks.without_fallbacks do 73 | assert_nil(p.title) 74 | end 75 | end 76 | end 77 | 78 | def test_assigns_in_specified_locale 79 | I18n.with_locale(:en) do 80 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 81 | p.title_de = 'Deutscher Titel' 82 | assert_equal('Deutscher Titel', p.title_raw['de']) 83 | end 84 | end 85 | 86 | def test_persists_changes_in_specified_locale 87 | I18n.with_locale(:en) do 88 | p = PageWithFallbacks.create!(:title_raw => {'en' => 'Original text'}) 89 | p.title_en = 'Updated text' 90 | p.save! 91 | assert_equal('Updated text', PageWithFallbacks.last.title_en) 92 | end 93 | end 94 | 95 | def test_retrieves_in_specified_locale 96 | I18n.with_locale(:en) do 97 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 98 | assert_equal('Deutscher Titel', p.title_de) 99 | end 100 | end 101 | 102 | def test_retrieves_in_specified_locale_with_fallbacks 103 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 104 | I18n.fallbacks = [:'en-US'] 105 | 106 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 107 | I18n.with_locale(:de) do 108 | assert_equal('English title', p.title_de) 109 | end 110 | end 111 | 112 | def test_fallback_from_empty_string 113 | I18n::Backend::Simple.include(I18n::Backend::Fallbacks) 114 | I18n.fallbacks = [:'en-US'] 115 | 116 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title', 'de' => ''}) 117 | I18n.with_locale(:de) do 118 | assert_equal('English title', p.title_de) 119 | end 120 | end 121 | 122 | def test_method_missing_delegates 123 | assert_raises(NoMethodError) { PageWithoutFallbacks.new.nonexistant_method } 124 | end 125 | 126 | def test_method_missing_delegates_non_translated_attributes 127 | assert_raises(NoMethodError) { PageWithoutFallbacks.new.other_de } 128 | end 129 | 130 | def test_persists_translations_assigned_as_hash 131 | p = PageWithoutFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 132 | assert_equal({'en' => 'English title', 'de' => 'Deutscher Titel'}, p.title_raw) 133 | end 134 | 135 | def test_persists_translations_assigned_to_localized_accessors 136 | p = PageWithFallbacks.create!(:title_en => 'English title', :title_de => 'Deutscher Titel') 137 | assert_equal({'en' => 'English title', 'de' => 'Deutscher Titel'}, p.title_raw) 138 | end 139 | 140 | def test_class_method_translates? 141 | assert_equal true, PageWithoutFallbacks.translates? 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/awesome_hstore_translate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AwesomeHstoreTranslateTest < AwesomeHstoreTranslate::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil AwesomeHstoreTranslate::VERSION 6 | end 7 | 8 | def test_translated_attributes 9 | attr_names = PageWithFallbacks.translated_attribute_names 10 | assert_equal([:title], attr_names) 11 | end 12 | 13 | def test_translated_accessors 14 | attr_names = PageWithFallbacks.translated_accessor_names 15 | assert_equal([:title_en, :title_de], attr_names) 16 | end 17 | 18 | def test_assigns_in_current_locale 19 | I18n.with_locale(:en) do 20 | p = PageWithoutFallbacks.new(:title => 'English title') 21 | assert_equal('English title', p.title_raw['en']) 22 | end 23 | end 24 | 25 | def test_retrieves_in_current_locale 26 | p = PageWithoutFallbacks.new(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 27 | I18n.with_locale(:de) do 28 | assert_equal('Deutscher Titel', p.title) 29 | end 30 | end 31 | 32 | def test_retrieves_in_current_locale_with_fallbacks 33 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 34 | I18n.fallbacks = [:'en-US'] 35 | 36 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 37 | I18n.with_locale(:de) do 38 | assert_equal('English title', p.title) 39 | end 40 | end 41 | 42 | def test_retrieves_in_current_locale_without_fallbacks 43 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 44 | I18n.fallbacks = [:'en'] 45 | 46 | p = PageWithoutFallbacks.new(:title_raw => {'en' => 'English title'}) 47 | I18n.with_locale(:de) do 48 | assert_nil(p.title) 49 | end 50 | end 51 | 52 | 53 | def test_retrieves_with_regional_enabled_fallbacks 54 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 55 | I18n.fallbacks[:en] = [:de] 56 | 57 | p = PageWithoutFallbacks.new(:title_raw => {'de' => 'Deutscher Titel'}) 58 | I18n.with_locale(:en) do 59 | PageWithoutFallbacks.with_fallbacks do 60 | assert_equal('Deutscher Titel', p.title) 61 | end 62 | end 63 | end 64 | 65 | 66 | def test_retrieves_with_regional_disabled_fallbacks 67 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 68 | I18n.fallbacks[:en] = [:de] 69 | 70 | p = PageWithFallbacks.new(:title_raw => {'de' => 'Deutscher Titel'}) 71 | I18n.with_locale(:en) do 72 | PageWithFallbacks.without_fallbacks do 73 | assert_nil(p.title) 74 | end 75 | end 76 | end 77 | 78 | def test_assigns_in_specified_locale 79 | I18n.with_locale(:en) do 80 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 81 | p.title_de = 'Deutscher Titel' 82 | assert_equal('Deutscher Titel', p.title_raw['de']) 83 | end 84 | end 85 | 86 | def test_persists_changes_in_specified_locale 87 | I18n.with_locale(:en) do 88 | p = PageWithFallbacks.create!(:title_raw => {'en' => 'Original text'}) 89 | p.title_en = 'Updated text' 90 | p.save! 91 | assert_equal('Updated text', PageWithFallbacks.last.title_en) 92 | end 93 | end 94 | 95 | def test_retrieves_in_specified_locale 96 | I18n.with_locale(:en) do 97 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 98 | assert_equal('Deutscher Titel', p.title_de) 99 | end 100 | end 101 | 102 | def test_retrieves_in_specified_locale_with_fallbacks 103 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 104 | I18n.fallbacks = [:'en-US'] 105 | 106 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title'}) 107 | I18n.with_locale(:de) do 108 | assert_equal('English title', p.title_de) 109 | end 110 | end 111 | 112 | def test_fallback_from_empty_string 113 | I18n::Backend::KeyValue.include(I18n::Backend::Fallbacks) 114 | I18n.fallbacks = [:'en-US'] 115 | 116 | p = PageWithFallbacks.new(:title_raw => {'en' => 'English title', 'de' => ''}) 117 | I18n.with_locale(:de) do 118 | assert_equal('English title', p.title_de) 119 | end 120 | end 121 | 122 | def test_method_missing_delegates 123 | assert_raises(NoMethodError) { PageWithoutFallbacks.new.nonexistant_method } 124 | end 125 | 126 | def test_method_missing_delegates_non_translated_attributes 127 | assert_raises(NoMethodError) { PageWithoutFallbacks.new.other_de } 128 | end 129 | 130 | def test_persists_translations_assigned_as_hash 131 | p = PageWithoutFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 132 | assert_equal({'en' => 'English title', 'de' => 'Deutscher Titel'}, p.title_raw) 133 | end 134 | 135 | def test_persists_translations_assigned_to_localized_accessors 136 | p = PageWithFallbacks.create!(:title_en => 'English title', :title_de => 'Deutscher Titel') 137 | assert_equal({'en' => 'English title', 'de' => 'Deutscher Titel'}, p.title_raw) 138 | end 139 | 140 | def test_class_method_translates? 141 | assert_equal true, PageWithoutFallbacks.translates? 142 | end 143 | 144 | def test_where_query_with_translated_value 145 | PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 146 | exp = PageWithFallbacks.create!(:title_raw => {'en' => 'Another English title', 'de' => 'Noch ein Deutscher Titel'}) 147 | res = PageWithFallbacks.where(title: 'Another English title').first 148 | assert_equal(exp.id, res.id) 149 | end 150 | 151 | def test_where_query_with_translated_value_and_other 152 | PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}, 153 | author: 'Awesome') 154 | exp = PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}, 155 | author: 'Spectacular') 156 | res = PageWithFallbacks.where(title: 'English title', author: 'Spectacular').first 157 | assert_equal(exp.id, res.id) 158 | end 159 | 160 | def test_fix_for_error_from_awesome_nested_table 161 | PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}, 162 | author: 'Awesome') 163 | exp = PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}, 164 | author: nil) 165 | res = PageWithFallbacks.where(PageWithFallbacks.arel_table[:author].eq(nil)).first 166 | assert_equal(exp.id, res.id) 167 | end 168 | 169 | def test_find_by_query_with_translated_value 170 | PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}) 171 | exp = PageWithFallbacks.create!(:title_raw => {'en' => 'Another English title', 'de' => 'Noch ein Deutscher Titel'}) 172 | res = PageWithFallbacks.find_by(title: 'Another English title') 173 | assert_equal(exp.id, res.id) 174 | end 175 | 176 | def test_first_or_create_on_where 177 | exp = 'Titre français' 178 | res = PageWithFallbacks.where(title: exp).first_or_create! 179 | refute_empty(res.id.to_s) 180 | # this shouldn't be nil 181 | assert_nil(res.title) 182 | 183 | exp = 'Titre français' 184 | res = PageWithFallbacks.where(title: exp).first_or_create!(title: exp) 185 | refute_empty(res.id.to_s) 186 | assert_equal(exp, res.title) 187 | end 188 | 189 | def test_find_or_create_by 190 | exp = 'Titre français' 191 | res = PageWithFallbacks.find_or_create_by(title: exp) 192 | refute_empty(res.id.to_s) 193 | assert_equal(exp, res.title) 194 | end 195 | 196 | def test_with_empty_translated_value 197 | new = PageWithFallbacks.new 198 | assert_nil(new.title) 199 | end 200 | 201 | def test_order_by 202 | PageWithFallbacks.create!(:title_raw => {'en' => 'English title', 'de' => 'Deutscher Titel'}, author: 'First') 203 | PageWithFallbacks.create!(:title_raw => {'en' => 'Another English title', 'de' => 'Noch ein Deutscher Titel'}, author: 'Second') 204 | PageWithFallbacks.create!(:title_raw => {'en' => 'Yet another English title', 'de' => 'Letzer Deutscher Titel'}, author: 'Third') 205 | 206 | res = PageWithFallbacks.all.order(title: :desc) 207 | assert_equal('Yet another English title', res.first.title) 208 | 209 | res = PageWithFallbacks.all.order(title: :asc) 210 | assert_equal('Another English title', res.first.title) 211 | 212 | res = PageWithFallbacks.all.order(:title) 213 | assert_equal('Another English title', res.first.title) 214 | 215 | I18n.with_locale(:de) do 216 | res = PageWithFallbacks.all.order(title: :desc) 217 | assert_equal('Noch ein Deutscher Titel', res.first.title) 218 | end 219 | 220 | I18n.with_locale(:de) do 221 | res = PageWithFallbacks.all.order(title: :asc) 222 | assert_equal('Deutscher Titel', res.first.title) 223 | end 224 | 225 | res = PageWithFallbacks.all.order(author: :desc) 226 | assert_equal('Third', res.first.author) 227 | 228 | res = PageWithFallbacks.all.order(author: :asc) 229 | assert_equal('First', res.first.author) 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | host: localhost 4 | database: postgres 5 | username: postgres 6 | password: postgres -------------------------------------------------------------------------------- /test/legacy_test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'minitest/autorun' 3 | require 'database_cleaner' 4 | require 'awesome_hstore_translate' 5 | require 'yaml' 6 | require 'models/PageWithoutFallbacks' 7 | require 'models/PageWithFallbacks' 8 | require 'byebug' 9 | 10 | DatabaseCleaner[:active_record].strategy = :transaction 11 | 12 | class AwesomeHstoreTranslate::Test < MiniTest::Test 13 | class << self 14 | def prepare_database 15 | create_database 16 | create_table 17 | end 18 | 19 | private 20 | 21 | def database_configuration 22 | @db_config ||= begin 23 | YAML.load_file(File.join('test', 'database.yml'))['test'] 24 | end 25 | end 26 | 27 | def establish_connection(config) 28 | ActiveRecord::Base.establish_connection(config) 29 | ActiveRecord::Base.connection 30 | end 31 | 32 | def create_database 33 | system_config = database_configuration.merge('database' => 'postgres', 'schema_search_path' => 'public') 34 | connection = establish_connection(system_config) 35 | connection.create_database(database_configuration['database']) rescue nil 36 | enable_extension 37 | end 38 | 39 | def enable_extension 40 | connection = establish_connection(database_configuration) 41 | unless connection.select_value("SELECT proname FROM pg_proc WHERE proname = 'akeys'") 42 | if connection.send(:postgresql_version) < 90100 43 | pg_sharedir = `pg_config --sharedir`.strip 44 | hstore_script_path = File.join(pg_sharedir, 'contrib', 'hstore.sql') 45 | connection.execute(File.read(hstore_script_path)) 46 | else 47 | connection.execute('CREATE EXTENSION IF NOT EXISTS hstore') 48 | end 49 | end 50 | end 51 | 52 | def create_table 53 | connection = establish_connection(database_configuration) 54 | connection.create_table(:page_with_fallbacks, :force => true) do |t| 55 | t.column :title_translations, 'hstore' 56 | end 57 | connection.create_table(:page_without_fallbacks, :force => true) do |t| 58 | t.column :title_translations, 'hstore' 59 | end 60 | end 61 | end 62 | 63 | prepare_database 64 | 65 | def setup 66 | I18n.available_locales = %w(en en-US de) 67 | I18n.config.enforce_available_locales = true 68 | DatabaseCleaner.start 69 | end 70 | 71 | def teardown 72 | DatabaseCleaner.clean 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/models/PageWithFallbacks.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | class PageWithFallbacks < ActiveRecord::Base 4 | translates :title, accessors: [:en, :de] 5 | end -------------------------------------------------------------------------------- /test/models/PageWithoutFallbacks.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | class PageWithoutFallbacks < ActiveRecord::Base 4 | translates :title, fallbacks: false 5 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'minitest/autorun' 3 | require 'database_cleaner' 4 | require 'awesome_hstore_translate' 5 | require 'yaml' 6 | require 'models/PageWithoutFallbacks' 7 | require 'models/PageWithFallbacks' 8 | require 'byebug' 9 | 10 | DatabaseCleaner[:active_record].strategy = :transaction 11 | 12 | class AwesomeHstoreTranslate::Test < MiniTest::Test 13 | class << self 14 | def prepare_database 15 | create_database 16 | create_table 17 | end 18 | 19 | private 20 | 21 | def database_configuration 22 | @db_config ||= begin 23 | YAML.load_file(File.join('test', 'database.yml'))['test'] 24 | end 25 | end 26 | 27 | def establish_connection(config) 28 | ActiveRecord::Base.establish_connection(config) 29 | ActiveRecord::Base.connection 30 | end 31 | 32 | def create_database 33 | system_config = database_configuration.merge('database' => 'postgres', 'schema_search_path' => 'public') 34 | connection = establish_connection(system_config) 35 | connection.create_database(database_configuration['database']) rescue nil 36 | enable_extension 37 | end 38 | 39 | def enable_extension 40 | connection = establish_connection(database_configuration) 41 | unless connection.select_value("SELECT proname FROM pg_proc WHERE proname = 'akeys'") 42 | if connection.send(:postgresql_version) < 90100 43 | pg_sharedir = `pg_config --sharedir`.strip 44 | hstore_script_path = File.join(pg_sharedir, 'contrib', 'hstore.sql') 45 | connection.execute(File.read(hstore_script_path)) 46 | else 47 | connection.execute('CREATE EXTENSION IF NOT EXISTS hstore') 48 | end 49 | end 50 | end 51 | 52 | def create_table 53 | connection = establish_connection(database_configuration) 54 | connection.create_table(:page_with_fallbacks, :force => true) do |t| 55 | t.column :title, 'hstore' 56 | t.column :author, :string 57 | end 58 | connection.create_table(:page_without_fallbacks, :force => true) do |t| 59 | t.column :title, 'hstore' 60 | t.column :author, :string 61 | end 62 | end 63 | end 64 | 65 | prepare_database 66 | 67 | def setup 68 | I18n.available_locales = %w(en en-US de) 69 | I18n.config.enforce_available_locales = true 70 | DatabaseCleaner.start 71 | end 72 | 73 | def teardown 74 | DatabaseCleaner.clean 75 | end 76 | end 77 | --------------------------------------------------------------------------------