├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app └── controllers │ └── translations_controller.rb ├── config ├── initializers │ └── babbel.rb └── routes.rb ├── inline_translation.gemspec ├── lib ├── generators │ └── inline_translation │ │ └── install │ │ ├── install_generator.rb │ │ └── templates │ │ ├── add_inline_translations.rb │ │ ├── create.js.erb │ │ └── inline_translation.rb ├── inline_translation.rb └── inline_translation │ ├── concerns │ ├── acts_as_translatable.rb │ └── translatable.rb │ ├── config │ └── routes.rb │ ├── engine.rb │ ├── helpers │ └── translations_helper.rb │ ├── models │ └── translation.rb │ ├── services │ └── translation_service.rb │ ├── translators │ ├── base.rb │ ├── bing.rb │ └── null.rb │ └── version.rb └── test ├── fixtures ├── application_controller.rb └── rails.rb ├── inline_translation_integration_test.rb ├── lib ├── concerns │ ├── acts_as_translatable_test.rb │ └── translatable_test.rb ├── controllers │ └── translations_controller_test.rb ├── generators │ └── babbel_generator_install_test.rb ├── helpers │ └── translations_helper_test.rb ├── models │ └── translation_test.rb ├── services │ └── translation_service_test.rb └── translators │ ├── base_test.rb │ ├── bing_test.rb │ └── null_test.rb ├── test_helper.rb └── test_types ├── controller_test.rb ├── integration_test.rb └── unit_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.gem 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in InlineTranslation.gemspec 4 | gemspec 5 | 6 | gem 'rails', '~> 4.1.0' 7 | gem 'bing_translator', '~> 4.4.0' 8 | 9 | group :development, :test do 10 | gem 'byebug', require: nil 11 | gem 'temping' 12 | gem 'mocha' 13 | gem 'sqlite3' 14 | end 15 | 16 | group :test do 17 | gem 'codeclimate-test-reporter', require: nil 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 James Kiesel 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inline Translation 2 | 3 | [![Join the chat at https://gitter.im/gdpelican/inline_translation](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gdpelican/inline_translation?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | ![Codeship](https://codeship.com/projects/50421e60-2297-0133-2512-365560d2eeeb/status?branch=master) [![Code Climate](https://img.shields.io/codeclimate/github/gdpelican/inline_translation.svg)](https://codeclimate.com/github/gdpelican/inline_translation) 5 | 6 | `InlineTranslation` is a gem which provides your application with a simple, easy-to-use way to perform inline translations of content, into a variety of languages. 7 | 8 | It provides automatic caching (and cache-busting!) mechanisms to ensure you never have to request a translation twice, or serve up a stale translation. 9 | 10 | It's written as a wrapper for the fine [bing_translator gem](https://github.com/relrod/bing_translator-gem), but can be easily extended to using other translation services with a little elbow grease. 11 | 12 | ## Demo 13 | 14 | Check out the example app [here](http://inline-translation-test.herokuapp.com) 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'inline_translation' 22 | ``` 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | Then, execute the install generator 29 | 30 | $ rails g inline_translation:install 31 | 32 | And migrate 33 | 34 | $ rake db:migrate 35 | 36 | Now you're all set up! 37 | 38 | ## Usage 39 | 40 | Inline Translation supplies several helper methods to make your translating life easier. 41 | 42 | To mark a field on an object as translatable, simply add 43 | 44 | `acts_as_translatable, on: :field_name` 45 | 46 | to your model. 47 | 48 | #### Additional options 49 | 50 | - **load_via** - the class method used to find a record for your model. Defaults to `:find` 51 | - **id_field** - field name for the unique identifier for your model. Defaults to `:id` 52 | - **language_field** - field name for the method / column name on your model to retrieve the language. Defaults to `language` 53 | 54 | NB: Oftentimes, you may wish to delegate this method to a user or other object, instead of storing the language on every model. 55 | 56 | For example: 57 | 58 | ```ruby 59 | # model.rb 60 | class Model < ActiveRecord::Base 61 | belongs_to :author, class_name: 'User' 62 | acts_as_translatable on: :column 63 | 64 | def language 65 | author.locale 66 | end 67 | end 68 | ``` 69 | 70 | ## On the frontend 71 | 72 | #### The translation link 73 | 74 | InlineTranslation provides a simple helper method for translation links in the view. 75 | 76 | For example, adding 77 | 78 | ```ruby 79 | translate_link_for(@model, to: :fr) 80 | ``` 81 | 82 | Will add an ajax link to create and store a French translation. The `to` field will default to I18n.locale. 83 | 84 | ###### Additional options 85 | 86 | - **text** - The text of the anchor generated. Defaults to 'Translate' 87 | - **to** - The language to translate to with this link. Defaults to I18n.locale 88 | 89 | (NB: This link will not appear if `@model.language` is equal to the 'to' parameter, since we cannot perform translations to the same language.) 90 | 91 | #### Populating the translation (via UJS) 92 | 93 | The simplest possible markup for including translations on callback: 94 | 95 | ```ruby 96 | # /app/views/models/show.html.erb 97 |
98 | <%= translated_element_for @model, :field_a %> 99 | <%= translated_element_for @model, :field_b %> 100 |
101 | ``` 102 | 103 | (Note that this markup can occur anywhere, as long as the `translated_element_for` elements are within a div of the format 'className-id') 104 | 105 | If this particular markup structure doesn't work for you for whatever reason, feel free to edit the `app/views/translations/create.js.erb` with javascript to your liking. 106 | 107 | ###### Additional options 108 | 109 | - **element** - The type of element generated. Defaults to 'span' 110 | 111 | #### Populating the translation (via JSON) 112 | 113 | The `translations#create` action can also accept a `:json` format, which will return a list of serialized translations. These can be consumed by your javascript frontend framework as you see fit. 114 | (TODO: provide more robust support for custom serialization, such as through ActiveModel::Serializers PRs welcome!) 115 | 116 | ie, a simplistic implementation in angular: 117 | 118 | ```html 119 | 120 | Translate 121 | ``` 122 | 123 | ```javascript 124 | // in the controller 125 | $scope.translateToFrench = function() { 126 | $http.post('/translations', { translatable_id: 1, translatable_type: 'Model', to: 'fr', format: 'json'}).then(function(data) { 127 | $scope.frenchTranslation = data 128 | }) 129 | } 130 | ``` 131 | 132 | 133 | ## On the backend 134 | 135 | InlineTranslation uses the Bing Translator API as a default. For instructions on setting up the Bing Translator API, [go here](https://github.com/relrod/bing_translator-gem#getting-a-client-id-and-secret). 136 | 137 | ## Different Translators 138 | 139 | Simply change the line in `config/initializers/inline_translation.rb` to use whatever translator you desire. 140 | 141 | Note that a custom translator must implement the following methods: 142 | 143 | - `self.ready?`: Returns true if the translator can translate anything 144 | - `can_translate?`: Returns true the translator can translate the given translatable 145 | - `translate`: Returns a translation for all `acts_as_translatable` fields on the translatable 146 | 147 | ## Contributing 148 | 149 | 1. Fork it ( https://github.com/gdpelican/inline_translation/fork ) 150 | 2. Create your feature branch (`git checkout -b my-new-feature`) 151 | 3. Commit your changes (`git commit -am 'Add some feature'`) 152 | 4. Push to the branch (`git push origin my-new-feature`) 153 | 5. Create a new Pull Request 154 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | task default: :test 5 | 6 | Rake::TestTask.new :test do |t| 7 | t.libs << 'lib' 8 | t.libs << 'test' 9 | t.pattern = 'test/**/*_test.rb' 10 | t.verbose = true 11 | t.warning = false 12 | end -------------------------------------------------------------------------------- /app/controllers/translations_controller.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Controllers 3 | class TranslationsController < ::ApplicationController 4 | respond_to :js, :json 5 | 6 | def create 7 | if service.translate(translatable, to: to_language) 8 | @translations = translatable.translations.where(language: to_language) 9 | respond_to do |format| 10 | format.js { render :create } 11 | format.json { render json: @translations } # TODO: support for AMS / custom serialization 12 | end 13 | else 14 | failure_response 15 | end 16 | end 17 | 18 | private 19 | 20 | def failure_response 21 | head :unprocessable_entity 22 | end 23 | 24 | def self.controller_path 25 | :translations 26 | end 27 | 28 | def service 29 | @service ||= InlineTranslation::Services::TranslationService.new 30 | end 31 | 32 | def translatable 33 | @translatable ||= params[:translatable_type].classify.constantize.get_instance params[:translatable_id] rescue nil 34 | end 35 | 36 | def to_language 37 | params[:to] || I18n.locale 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/babbel.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:active_record) { include InlineTranslation::Concerns::ActsAsTranslatable } 2 | ActiveSupport.on_load(:action_view) { include InlineTranslation::Helpers::TranslationsHelper } 3 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :translations, only: :create, module: 'inline_translation/controllers', as: :translations 3 | end -------------------------------------------------------------------------------- /inline_translation.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'inline_translation/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "inline_translation" 8 | spec.version = InlineTranslation::VERSION 9 | spec.authors = ["James Kiesel (gdpelican)"] 10 | spec.email = ["james@loomio.org"] 11 | 12 | spec.summary = "Store on-the-fly translations using Bing (or others!)" 13 | spec.description = "Sets up a framework for allowing inline translation of database content" 14 | spec.homepage = "https://github.com/gdpelican/inline_translation" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["app", "lib"] 21 | 22 | spec.add_runtime_dependency "rails", "~> 4.1" 23 | spec.add_runtime_dependency "bing_translator", "~> 4.4" 24 | 25 | spec.add_development_dependency "bundler", "~> 1.7" 26 | spec.add_development_dependency "rake", "~> 10.0" 27 | spec.add_development_dependency "minitest", "~> 5.7" 28 | end 29 | -------------------------------------------------------------------------------- /lib/generators/inline_translation/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | class InstallGenerator < Rails::Generators::Base 3 | include Rails::Generators::Migration 4 | desc "Adds InlineTranslation translations table & initializer" 5 | source_root File.expand_path '../templates', __FILE__ 6 | 7 | def copy_migration 8 | migration_template "add_inline_translations.rb", "db/migrate/add_inline_translations.rb" 9 | end 10 | 11 | def copy_initializer 12 | copy_file "inline_translation.rb", "config/initializers/inline_translation.rb" 13 | end 14 | 15 | def copy_js_view 16 | copy_file "create.js.erb", "app/views/translations/create.js.erb" 17 | end 18 | 19 | def self.next_migration_number(path) 20 | @previous_migration_nr ||= Time.now.utc.strftime("%Y%m%d%H%M%S").to_i 21 | @previous_migration_nr += 1 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/inline_translation/install/templates/add_inline_translations.rb: -------------------------------------------------------------------------------- 1 | class AddInlineTranslations < ActiveRecord::Migration 2 | def self.up 3 | create_table :translations do |t| 4 | t.integer :translatable_id 5 | t.string :translatable_type 6 | t.string :field 7 | t.string :language 8 | t.text :translation 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :translations 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/inline_translation/install/templates/create.js.erb: -------------------------------------------------------------------------------- 1 | target = $('#<%= @translatable.class.to_s.downcase %>-<%= @translatable.id %>') 2 | <% @translations.each do |translation| %> 3 | target.find('.inline-translation-translated.<%= translation.field %>-translated').html("<%= j translation.translation %>") 4 | <% end %> 5 | -------------------------------------------------------------------------------- /lib/generators/inline_translation/install/templates/inline_translation.rb: -------------------------------------------------------------------------------- 1 | InlineTranslation.translator = InlineTranslation::Translators::Bing 2 | -------------------------------------------------------------------------------- /lib/inline_translation.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'inline_translation/engine' 3 | 4 | module InlineTranslation 5 | extend ActiveSupport::Autoload 6 | 7 | cattr_accessor :translator 8 | 9 | module Concerns 10 | autoload :ActsAsTranslatable, 'inline_translation/concerns/acts_as_translatable' 11 | autoload :Translatable, 'inline_translation/concerns/translatable' 12 | end 13 | 14 | module Controllers 15 | autoload :TranslationsController, 'controllers/translations_controller' 16 | end 17 | 18 | module Generators 19 | autoload :InstallGenerator, 'generators/install/install_generator' 20 | end 21 | 22 | module Helpers 23 | autoload :TranslationsHelper, 'inline_translation/helpers/translations_helper' 24 | end 25 | 26 | module Models 27 | autoload :Translation, 'inline_translation/models/translation' 28 | end 29 | 30 | module Services 31 | autoload :TranslationService, 'inline_translation/services/translation_service' 32 | end 33 | 34 | module Translators 35 | autoload :Base, 'inline_translation/translators/base' 36 | autoload :Bing, 'inline_translation/translators/bing' 37 | autoload :Null, 'inline_translation/translators/null' 38 | end 39 | 40 | self.translator ||= Translators::Null 41 | 42 | end 43 | -------------------------------------------------------------------------------- /lib/inline_translation/concerns/acts_as_translatable.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Concerns 3 | module ActsAsTranslatable 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | def acts_as_translatable(on: [], load_via: :find, id_field: :id, language_field: :language) 8 | include InlineTranslation::Concerns::Translatable 9 | define_singleton_method :translatable_fields, -> { Array on } 10 | define_singleton_method :get_instance, ->(id) { send load_via, id } 11 | define_method :id_field, -> { send id_field } 12 | define_method :language_field, -> { send language_field } 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/inline_translation/concerns/translatable.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Concerns 3 | module Translatable 4 | extend ActiveSupport::Concern 5 | included do 6 | has_many :translations, as: :translatable, class_name: 'InlineTranslation::Models::Translation' 7 | before_update :destroy_modified_translations 8 | 9 | private 10 | 11 | def destroy_modified_translations 12 | translations.each { |t| t.destroy if changed.include? t.field } 13 | end 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/inline_translation/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :translations, only: :create 3 | end -------------------------------------------------------------------------------- /lib/inline_translation/engine.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | class Engine < ::Rails::Engine 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/inline_translation/helpers/translations_helper.rb: -------------------------------------------------------------------------------- 1 | require 'action_view/helpers' 2 | 3 | module InlineTranslation 4 | module Helpers 5 | module TranslationsHelper 6 | include ActionView::Helpers 7 | 8 | def translate_link_for(translatable, to: I18n.locale, text: "Translate") 9 | link_to text, path_for(translatable, to), method: :post, remote: true if translatable.language != to 10 | end 11 | 12 | def translated_element_for(translatable, field, element: :span, to: I18n.locale) 13 | content_tag element, '', class: "#{field}-translated to-#{to} inline-translation-translated" 14 | end 15 | 16 | private 17 | 18 | def path_for(translatable, to) 19 | translations_path translatable_id: translatable.id, 20 | translatable_type: translatable.class.to_s, 21 | to: to, 22 | action: :create 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/inline_translation/models/translation.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Models 3 | class Translation < ActiveRecord::Base 4 | belongs_to :translatable, polymorphic: true 5 | scope :to_language, ->(language) { where language: language } 6 | validates_presence_of :translatable, :language, :field, :translation 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/inline_translation/services/translation_service.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Services 3 | class TranslationService 4 | attr_reader :translator 5 | 6 | def initialize(translator_class = InlineTranslation.translator) 7 | raise InvalidTranslatorError.new unless translator_class.ready? 8 | @translator = translator_class.new 9 | end 10 | 11 | def translate(translatable, to: I18n.locale) 12 | translate!(translatable, to: to) rescue false 13 | end 14 | 15 | def translate!(translatable, to: I18n.locale) 16 | translatable.class.translatable_fields.map { |field| translate_field(translatable, field, to: to) } 17 | translatable.save 18 | end 19 | 20 | def translate_field(translatable, field, to: I18n.locale) 21 | translatable.translations.build( 22 | field: field, 23 | language: to, 24 | translation: @translator.translate(translatable.send(field), from: translatable.language_field, to: to) 25 | ) if @translator.can_translate?(translatable, field, to) 26 | end 27 | 28 | end 29 | 30 | class InvalidTranslatorError < StandardError 31 | def to_s 32 | "Unable to instantiate translator: Please ensure that the appropriate ENV variables are set" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/inline_translation/translators/base.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Translators 3 | class Base 4 | attr_reader :translator 5 | 6 | def self.ready? 7 | false 8 | end 9 | 10 | def can_translate?(translatable, field, to) 11 | self.class.ready? && 12 | to.present? && 13 | translatable.respond_to?(field) && 14 | translatable.language_field.present? && 15 | translatable.language_field.to_s != to.to_s && 16 | translatable.translations.where(field: field, language: to).empty? 17 | end 18 | 19 | def translate(text, from: nil, to: I18n.locale) 20 | raise NotImplementedError.new 21 | end 22 | 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/inline_translation/translators/bing.rb: -------------------------------------------------------------------------------- 1 | require 'bing_translator' 2 | 3 | module InlineTranslation 4 | module Translators 5 | class Bing < Base 6 | 7 | def self.ready? 8 | ENV['BING_TRANSLATOR_APP_ID'] && ENV['BING_TRANSLATOR_SECRET'] && true 9 | end 10 | 11 | def initialize 12 | @translator = ::BingTranslator.new ENV['BING_TRANSLATOR_APP_ID'], ENV['BING_TRANSLATOR_SECRET'] 13 | end 14 | 15 | def translate(text, from: nil, to: I18n.locale) 16 | @translator.translate text, from: from, to: to 17 | end 18 | 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/inline_translation/translators/null.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | module Translators 3 | class Null < Base 4 | 5 | def self.ready? 6 | true 7 | end 8 | 9 | def initialize 10 | end 11 | 12 | def translate(text, from: nil, to: I18n.locale) 13 | nil 14 | end 15 | 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/inline_translation/version.rb: -------------------------------------------------------------------------------- 1 | module InlineTranslation 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | define_method :_routes, ->{} 3 | end -------------------------------------------------------------------------------- /test/fixtures/rails.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | def self.application 3 | OpenStruct.new routes: routes, 4 | env_config: {} 5 | end 6 | 7 | def self.root 8 | Dir['../../../'] 9 | end 10 | 11 | def self.env 12 | OpenStruct.new to_s: "test", 13 | development?: false, 14 | test?: true, 15 | production?: false 16 | end 17 | 18 | def self.backtrace_cleaner 19 | ActiveSupport::BacktraceCleaner.new 20 | end 21 | 22 | class Engine 23 | end 24 | 25 | private 26 | 27 | def self.routes 28 | @routes ||= ActionDispatch::Routing::RouteSet.new.tap do |routes| 29 | routes.draw { resources :translations } 30 | end 31 | end 32 | 33 | def self.draw_routes 34 | 35 | end 36 | end -------------------------------------------------------------------------------- /test/inline_translation_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | class InlineTranslationIntegrationTest < IntegrationTest 3 | describe InlineTranslation do 4 | setup_model :integration_model 5 | 6 | let(:model) { IntegrationModel.create! column1: "column one", column2: "column2", language: :en } 7 | 8 | setup do 9 | @controller ||= InlineTranslation::Controllers::TranslationsController.new 10 | IntegrationModel.acts_as_translatable on: [:column1, :column2] 11 | InlineTranslation::Translators::Null.stubs(:ready?).returns(true) 12 | InlineTranslation::Translators::Null.any_instance.stubs(:translate).returns("this is a translation", "this is another translation") 13 | end 14 | 15 | describe "creating translations" do 16 | it "can create translations" do 17 | post :create, translatable_type: "IntegrationModel", translatable_id: model.id, to: :fr, format: :json 18 | 19 | created = Translation.where(translatable_type: "IntegrationModel") 20 | 21 | assert_equal created.where(translatable_id: model.id).size, 2 22 | assert_equal created.where(language: :fr).size, 2 23 | assert_equal created.where(translatable_type: "IntegrationModel").size, 2 24 | assert_equal created.where(translation: "this is a translation").size, 1 25 | assert_equal created.where(translation: "this is another translation").size, 1 26 | 27 | 28 | before_count = Translation.count 29 | post :create, translatable_type: "IntegrationModel", translatable_id: model.id, to: :fr, format: :json 30 | assert_equal Translation.count, before_count 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/lib/concerns/acts_as_translatable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ActsAsTranslatableTest < UnitTest 4 | describe InlineTranslation::Concerns::ActsAsTranslatable do 5 | 6 | before do 7 | setup_model :concern_model 8 | ConcernModel.define_singleton_method(:find_alt) { |id| "found #{id}!" } 9 | end 10 | 11 | it "includes Translatable" do 12 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2]" 13 | assert ConcernModel.included_modules.include?(InlineTranslation::Concerns::Translatable) 14 | end 15 | 16 | it "defines a translatable_fields class method" do 17 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2]" 18 | assert_equal ConcernModel.translatable_fields, [:column1, :column2] 19 | end 20 | 21 | it "defines a single translatable_field correctly" do 22 | ConcernModel.class_eval "acts_as_translatable on: :column1" 23 | assert_equal ConcernModel.translatable_fields, [:column1] 24 | end 25 | 26 | it "defines a custom get_instance class method" do 27 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2], load_via: :find_alt" 28 | assert_equal ConcernModel.get_instance(42), ConcernModel.find_alt(42) 29 | end 30 | 31 | it "defines a get_instance class method as :find by default" do 32 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2]" 33 | model = ConcernModel.create 34 | assert_equal ConcernModel.get_instance(model.id), ConcernModel.find(model.id) 35 | end 36 | 37 | it "defines an id_field method as :id by default" do 38 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2]" 39 | model = ConcernModel.new id: 42 40 | assert_equal model.id_field, model.id 41 | end 42 | 43 | it "defines a custom id_field method" do 44 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2], id_field: :id_alt" 45 | model = ConcernModel.new id_alt: 42 46 | assert_equal model.id_field, model.id_alt 47 | end 48 | 49 | it "defines a language_field method as :language by default" do 50 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2]" 51 | model = ConcernModel.new language: :en 52 | assert_equal model.language_field, model.language 53 | end 54 | 55 | it "defines a custom language_field method" do 56 | ConcernModel.class_eval "acts_as_translatable on: [:column1, :column2], language_field: :language_alt" 57 | model = ConcernModel.new language_alt: :en 58 | assert_equal model.language_field, model.language_alt 59 | end 60 | end 61 | 62 | end -------------------------------------------------------------------------------- /test/lib/concerns/translatable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranslatableTest < UnitTest 4 | describe InlineTranslation::Concerns::Translatable do 5 | 6 | let(:model) { ConcernModel.create column1: "test text" } 7 | 8 | before do 9 | setup_model :concern_model 10 | include_translatable ConcernModel 11 | end 12 | 13 | it "has_many translations" do 14 | assert_respond_to model, :translations 15 | assert_instance_of InlineTranslation::Models::Translation, model.translations.build 16 | end 17 | 18 | it "destroys translations after update" do 19 | model.translations.build language: :en, field: :column1, translation: "test translation" 20 | model.save 21 | assert_equal model.reload.translations.size, 1 22 | 23 | model.update! column1: "changed text" 24 | model.save 25 | 26 | assert_equal model.reload.translations.size, 0 27 | end 28 | 29 | end 30 | 31 | end -------------------------------------------------------------------------------- /test/lib/controllers/translations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranslationsControllerTest < ControllerTest 4 | describe InlineTranslation::Controllers::TranslationsController do 5 | setup_model :controller_model 6 | let(:service) { InlineTranslation::Services::TranslationService.new } 7 | let(:translatable) { ControllerModel.create } 8 | let(:translation_result) { { column1: 'A translation!', column2: 'A translation!' } } 9 | 10 | setup do 11 | ControllerModel.class_eval "acts_as_translatable on: [:column1, :column2]" 12 | @controller ||= InlineTranslation::Controllers::TranslationsController.new 13 | translatable 14 | end 15 | 16 | describe "POST create" do 17 | it "returns the translation for successful translation for JSON" do 18 | InlineTranslation.stubs(:ready?).returns(true) 19 | InlineTranslation::Translators::Null.any_instance.stubs(:can_translate?).returns(true) 20 | InlineTranslation::Translators::Null.any_instance.stubs(:translate).returns("A translation!") 21 | post :create, translatable_type: "ControllerModel", translatable_id: translatable.id, format: :json 22 | 23 | assert_equal response.status, 200 24 | json = JSON.parse(response.body) 25 | 26 | assert_equal json.length, translatable.translations.size 27 | fields = json.map { |t| t['field'] } 28 | translatable_ids = json.map { |t| t['translatable_id'] } 29 | translatable_types = json.map { |t| t['translatable_type'] } 30 | 31 | assert_includes fields, 'column1' 32 | assert_includes fields, 'column2' 33 | assert_includes translatable_ids, translatable.id 34 | assert_includes translatable_types, 'ControllerModel' 35 | end 36 | 37 | it "returns the translation for successful translation for JS" do 38 | # TODO: stub out the call to render, which errors because we don't have a translations#create view 39 | skip "Stub out render call so there's no 'cannot find translations#create view' error" 40 | InlineTranslation::Translators::Null.any_instance.stubs(:can_translate?).returns(true) 41 | InlineTranslation::Translators::Null.any_instance.stubs(:translate).returns("A translation!") 42 | ActionView::Renderer.any_instance.stubs(:render).with('translations/create').returns('wark') 43 | post :create, translatable_type: "ControllerModel", translatable_id: translatable.id, format: :js 44 | 45 | assert_equal response.status, 200 46 | end 47 | 48 | 49 | it "returns unprocessable entity for unsuccessful translation" do 50 | InlineTranslation::Services::TranslationService.any_instance.stubs(:translate).returns(false) 51 | post :create, translatable_type: "ControllerModel", translatable_id: translatable.id, format: :json 52 | assert_equal response.status, 422 53 | end 54 | 55 | it "returns unprocessable entity when translatable_type is not defined" do 56 | InlineTranslation::Services::TranslationService.any_instance.stubs(:translate).returns(false) 57 | post :create, format: :json 58 | assert_equal response.status, 422 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/lib/generators/babbel_generator_install_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/inline_translation/install/install_generator' 3 | 4 | class InlineTranslationGeneratorInstallTest < Rails::Generators::TestCase 5 | tests InlineTranslation::InstallGenerator 6 | setup_destination 7 | 8 | test "generates a migration file" do 9 | run_generator 10 | assert_migration "db/migrate/add_inline_translations.rb" 11 | end 12 | 13 | test "generates an initializer file" do 14 | run_generator 15 | assert_file "config/initializers/inline_translation.rb" 16 | end 17 | 18 | test "generates a create js view file" do 19 | run_generator 20 | assert_file "app/views/translations/create.js.erb" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/lib/helpers/translations_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TranslationsHelperTest < UnitTest 4 | include InlineTranslation::Helpers::TranslationsHelper 5 | include Rails.application.routes.url_helpers 6 | 7 | setup_model :helper_model 8 | 9 | let(:model) { HelperModel.new column1: 'a value', language: :en } 10 | 11 | before do 12 | I18n.locale = :fr 13 | end 14 | 15 | describe ".translate_link_for" do 16 | it "returns a link with another locale set" do 17 | assert_match /\?to=fr/, translate_link_for(model) 18 | end 19 | 20 | it "returns a link with a specified locale" do 21 | I18n.locale = :en 22 | assert_match /\?to=fr/, translate_link_for(model, to: :fr) 23 | end 24 | 25 | it "defaults to 'Translate' for link text" do 26 | assert_match /Translate/, translate_link_for(model) 27 | end 28 | 29 | it "accepts a text parameter" do 30 | assert_match /Other text/, translate_link_for(model, text: 'Other text') 31 | end 32 | 33 | it "does not return a link when locale is the same" do 34 | I18n.locale = :en 35 | refute translate_link_for(model) 36 | end 37 | 38 | it "does not return a link when specified locale is the same" do 39 | refute translate_link_for(model, to: :en) 40 | end 41 | end 42 | 43 | describe ".translated_element_for" do 44 | it "returns an span with a language class" do 45 | assert_match /to-fr/, translated_element_for(model, :column1) 46 | end 47 | 48 | it "returns a span with a field class" do 49 | assert_match /column1-translated/, translated_element_for(model, :column1) 50 | end 51 | 52 | it "returns a span with a InlineTranslation class" do 53 | assert_match /inline-translation-translated/, translated_element_for(model, :column1) 54 | end 55 | 56 | it "accepts an element parameter" do 57 | assert_match /
{ InlineTranslation::Services::TranslationService.new(translator_class) }.must_raise InlineTranslation::Services::InvalidTranslatorError 28 | end 29 | end 30 | 31 | describe "invalid translator error" do 32 | it "has an error message" do 33 | assert_match /Unable to instantiate translator/, InlineTranslation::Services::InvalidTranslatorError.new.to_s 34 | end 35 | end 36 | 37 | describe "translate" do 38 | it "returns the results of translate!" do 39 | service.stubs(:translate!).returns("translation result") 40 | assert_equal service.translate(translatable), "translation result" 41 | end 42 | end 43 | 44 | describe "translate!" do 45 | it "builds all translations using translate_field" do 46 | service.translate!(translatable) 47 | assert_equal translatable.translations.size, 2 48 | end 49 | 50 | it "does not build an invalid translation" do 51 | service.translator.stubs(:can_translate?).returns(false) 52 | service.translate!(translatable) 53 | assert_equal translatable.translations.size, 0 54 | end 55 | end 56 | 57 | describe "translate_field" do 58 | it "builds a translation with translate_field" do 59 | service.translate_field(translatable, :column1) 60 | assert_equal translatable.translations.size, 1 61 | end 62 | it "does not build an invalid translation" do 63 | service.translator.stubs(:can_translate?).returns(false) 64 | service.translate_field(translatable, :column1) 65 | assert_equal translatable.translations.size, 0 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/lib/translators/base_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'inline_translation/translators/base' 3 | 4 | class BaseTest < UnitTest 5 | describe InlineTranslation::Translators::Base do 6 | setup_model :translator_model 7 | 8 | let(:translator_class) { InlineTranslation::Translators::Base } 9 | let(:translator) { translator_class.new } 10 | let(:translatable) { TranslatorModel.new column1: "column one", language: :en } 11 | 12 | setup do 13 | TranslatorModel.acts_as_translatable on: :column1 14 | end 15 | 16 | it "returns false on ready?" do 17 | refute translator_class.ready? 18 | end 19 | 20 | describe ".can_translate?" do 21 | 22 | it "returns false if the translator is not ready" do 23 | refute translator.can_translate? translatable, :column1, :fr 24 | end 25 | 26 | it "returns false if 'to' is not set" do 27 | translator.stubs(:ready?).returns(:true) 28 | refute translator.can_translate? translatable, :column1, nil 29 | end 30 | 31 | it "returns false if translatable's language is not set" do 32 | translator.stubs(:ready?).returns(:true) 33 | translatable.language = nil 34 | refute translator.can_translate? translatable, :column1, :fr 35 | end 36 | 37 | it "returns false if the from and to language are the same" do 38 | translator.stubs(:ready?).returns(:true) 39 | refute translator.can_translate? translatable, :column1, :en 40 | end 41 | 42 | it "returns false if a translation already exists" do 43 | translator.stubs(:ready?).returns(:true) 44 | translatable.save 45 | translatable.translations.create field: :column1, language: :fr 46 | refute translator.can_translate? translatable, :column1, :fr 47 | end 48 | 49 | it "returns false if the field is not found on translatable" do 50 | translator_class.stubs(:ready?).returns(:true) 51 | refute translator.can_translate? translatable, :notacolumn, :fr 52 | end 53 | 54 | it "returns true otherwise" do 55 | translator_class.stubs(:ready?).returns(:true) 56 | assert translator.can_translate? translatable, :column1, :fr 57 | end 58 | end 59 | 60 | it "raises an error on translate" do 61 | assert_raises NotImplementedError do translator.translate(nil) end 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/lib/translators/bing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'inline_translation/translators/base' 3 | require 'inline_translation/translators/bing' 4 | require 'bing_translator' 5 | 6 | class BingTest < UnitTest 7 | describe InlineTranslation::Translators::Bing do 8 | 9 | let(:translator_class) { InlineTranslation:: Translators::Bing } 10 | let(:translator) { translator_class.new } 11 | 12 | before do 13 | setup_bing_translator_env 14 | end 15 | 16 | it "initializes a BingTranslator" do 17 | assert_instance_of BingTranslator, translator.translator 18 | end 19 | 20 | it "returns ready if ENV variables are set" do 21 | assert translator_class.ready? 22 | end 23 | 24 | it "returns not ready if app id is not set" do 25 | ENV['BING_TRANSLATOR_APP_ID'] = nil 26 | refute translator_class.ready? 27 | end 28 | 29 | it "returns not ready if secret is not set" do 30 | ENV['BING_TRANSLATOR_SECRET'] = nil 31 | refute translator_class.ready? 32 | end 33 | 34 | it "can translate a translatable" do 35 | translator.translator.stubs(:translate).returns("translation") 36 | assert_equal translator.translate("original"), "translation" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/lib/translators/null_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'inline_translation/translators/base' 3 | require 'inline_translation/translators/null' 4 | 5 | class NullTest < UnitTest 6 | describe InlineTranslation::Translators::Null do 7 | 8 | let(:translator_class) { InlineTranslation::Translators::Null } 9 | let(:translator) { translator_class.new } 10 | 11 | it "returns ready as true" do 12 | assert translator_class.ready? 13 | end 14 | 15 | it "returns nil as a translation" do 16 | assert_nil translator.translate("anything") 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | 3 | require "codeclimate-test-reporter" 4 | CodeClimate::TestReporter.start 5 | 6 | require 'byebug' 7 | require 'bundler/setup' 8 | require 'active_record' 9 | require 'temping' 10 | ActiveRecord::Base.establish_connection adapter: :sqlite3, database: ':memory:' 11 | 12 | require 'action_controller' 13 | 14 | require 'fixtures/rails' 15 | require 'fixtures/application_controller' 16 | 17 | require 'minitest/autorun' 18 | require 'minitest/pride' 19 | require 'mocha/mini_test' 20 | require 'rails/test_help' 21 | 22 | require 'test_types/unit_test' 23 | require 'test_types/controller_test' 24 | require 'test_types/integration_test' 25 | 26 | require 'inline_translation' 27 | 28 | I18n.enforce_available_locales = false 29 | 30 | def setup_destination 31 | destination File.expand_path '../../../tmp', __FILE__ 32 | setup :prepare_destination 33 | end 34 | 35 | def setup_model(model = :test_model) 36 | constantized = model.to_s.split("_").collect(&:capitalize).join 37 | unless Object.const_defined?(constantized) 38 | Temping.create model do 39 | with_columns do |t| 40 | t.integer :id_alt 41 | t.string :column1, :column2, :language, :language_alt 42 | end 43 | end 44 | include_acts_as_translatable Object.const_get(constantized) 45 | end 46 | end 47 | 48 | def setup_translation 49 | unless Object.const_defined?("Translation") 50 | Temping.create :translation do 51 | with_columns do |t| 52 | t.integer :translatable_id 53 | t.string :translatable_type 54 | t.string :field 55 | t.string :language 56 | t.text :translation 57 | t.timestamps 58 | end 59 | end 60 | end 61 | end 62 | 63 | def setup_bing_translator_env 64 | ENV['BING_TRANSLATOR_APP_ID'] = 'set' 65 | ENV['BING_TRANSLATOR_SECRET'] = 'set' 66 | end 67 | 68 | def include_acts_as_translatable(model) 69 | model.class_eval "include InlineTranslation::Concerns::ActsAsTranslatable" 70 | end 71 | 72 | def include_translatable(model) 73 | model.class_eval "include InlineTranslation::Concerns::Translatable" 74 | end 75 | -------------------------------------------------------------------------------- /test/test_types/controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'action_controller' 2 | 3 | class ControllerTest < UnitTest 4 | include ActionController::TestCase::Behavior 5 | before { @routes = Rails.application.routes } 6 | end -------------------------------------------------------------------------------- /test/test_types/integration_test.rb: -------------------------------------------------------------------------------- 1 | class IntegrationTest < ControllerTest 2 | end -------------------------------------------------------------------------------- /test/test_types/unit_test.rb: -------------------------------------------------------------------------------- 1 | class UnitTest < MiniTest::Spec 2 | include ActiveSupport::Testing::SetupAndTeardown 3 | end --------------------------------------------------------------------------------