#--
# Copyright (c) 2010-2011 Michael Berkovich, tr8n.net
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'digest/md5'
class Tr8n::TranslationKey < ActiveRecord::Base
set_table_name :tr8n_translation_keys
after_save :clear_cache
after_destroy :clear_cache
has_many :translations, :class_name => "Tr8n::Translation", :dependent => :destroy
has_many :translation_key_locks, :class_name => "Tr8n::TranslationKeyLock", :dependent => :destroy
has_many :translation_key_sources, :class_name => "Tr8n::TranslationKeySource", :dependent => :destroy
has_many :translation_sources, :class_name => "Tr8n::TranslationSource", :through => :translation_key_sources
has_many :translation_domains, :class_name => "Tr8n::TranslationDomain", :through => :translation_sources
has_many :translation_key_comments, :class_name => "Tr8n::TranslationKeyComment", :dependent => :destroy, :order => "created_at desc"
alias :locks :translation_key_locks
alias :key_sources :translation_key_sources
alias :sources :translation_sources
alias :domains :translation_domains
alias :comments :translation_key_comments
def self.find_or_create(label, desc = "", options = {})
key = generate_key(label, desc).to_s
tkey = Tr8n::Cache.fetch("translation_key_#{key}") do
existing_key = where(:key => key).first
unless existing_key
if options[:api] and (not Tr8n::Config.api[:allow_key_registration])
raise Tr8n::KeyRegistrationException.new("Key registration through API is disabled!")
end
end
level = options[:level] || Tr8n::Config.block_options[:level] || Tr8n::Config.default_translation_key_level
role_key = options[:role] || Tr8n::Config.block_options[:role]
if role_key # role overrides level
level = Tr8n::Config.translator_roles[role_key]
raise Tr8n::Exception("Unknown translator role: #{role_key}") unless level
end
locale = options[:locale] || Tr8n::Config.block_options[:default_locale] || Tr8n::Config.default_locale
existing_key ||= begin
new_tkey = create(:key => key.to_s,
:label => label,
:description => desc,
:locale => locale,
:level => level,
:admin => Tr8n::Config.block_options[:admin])
unless options[:source].blank?
# at the time of creation - mark the first source of the key
Tr8n::TranslationKeySource.find_or_create(new_tkey, Tr8n::TranslationSource.find_or_create(options[:source], options[:url]))
end
new_tkey
end
# for backwards compatibility only
mark_as_admin(existing_key, options)
update_default_locale(existing_key, options)
# mark each key as verified - but only if caching is enabled
existing_key.update_attributes(:verified_at => Time.now) if Tr8n::Config.enable_caching?
existing_key
end
# for detailed tracking of all sources and caller stack - only needed for debugging - expensive
track_source(tkey, options)
tkey
end
# for backwards compatibility only - new keys will be marked as such
def self.mark_as_admin(tkey, options)
return if options[:skip_block_options]
return unless Tr8n::Config.block_options[:admin]
return if tkey.admin?
tkey.update_attributes(:admin => true)
end
# for backwards compatibility only - if locale is provided update it in the key
def self.update_default_locale(tkey, options)
return if options[:skip_block_options]
return unless tkey.locale.blank?
key_locale = Tr8n::Config.block_options[:default_locale] || Tr8n::Config.default_locale
tkey.update_attributes(:locale => key_locale)
end
# used to create associations between the translation keys and source
# primarely used for the site map and only needs to be enabled
# for a short period of time on a single machine
def self.track_source(tkey, options)
return unless Tr8n::Config.enable_key_source_tracking?
return if options[:source].blank?
key_source = Tr8n::TranslationKeySource.find_or_create(tkey, Tr8n::TranslationSource.find_or_create(options[:source], options[:url]))
return unless Tr8n::Config.enable_key_caller_tracking?
options[:caller] ||= caller
options[:caller_key] = options[:caller].is_a?(Array) ? options[:caller].join(", ") : options[:caller].to_s
options[:caller_key] = generate_key(options[:caller_key])
key_source.update_details!(options)
end
def self.generate_key(label, desc = "")
# TODO: there is something iffy going on with the strings from the hash
# without the extra ~ = the strings are not seen in the sqlite database - wtf?
"#{Digest::MD5.hexdigest("#{label};;;#{desc}")}~"[0..-2]
end
def reset_key!
# remove old key from cache
Tr8n::Cache.delete("translation_key_#{key}")
self.update_attributes(:key => self.class.generate_key(label, description))
end
def language
@language ||= (locale ? Tr8n::Language.for(locale) : Tr8n::Config.default_language)
end
def tokenized_label
@tokenized_label ||= Tr8n::TokenizedLabel.new(label)
end
delegate :tokens, :tokens?, :to => :tokenized_label
delegate :data_tokens, :data_tokens?, :to => :tokenized_label
delegate :decoration_tokens, :decoration_tokens?, :to => :tokenized_label
delegate :translation_tokens, :translation_tokens?, :to => :tokenized_label
delegate :sanitized_label, :tokenless_label, :suggestion_tokens, :words, :to => :tokenized_label
# comments are left for a specific language
def comments(language = Tr8n::Config.current_language)
Tr8n::TranslationKeyComment.where("language_id = ? and translation_key_id = ?", language.id, self.id)
end
# returns only the tokens that depend on one or more rules of the language, if any defined for the language
def language_rules_dependant_tokens(language = Tr8n::Config.current_language)
toks = []
included_token_hash = {}
data_tokens.each do |token|
next unless token.dependant?
next if included_token_hash[token.name]
token.language_rules.each do |rule_class|
if language.rule_class_names.include?(rule_class.name)
toks << token
included_token_hash[token.name] = token
end
end
end
toks << Tr8n::Config.viewing_user_token_for(label) if language.gender_rules?
toks.uniq
end
# determines whether the key can have rules generated for the language
def permutatable?(language = Tr8n::Config.current_language)
language_rules_dependant_tokens(language).any?
end
def glossary
@glossary ||= Tr8n::Glossary.where("keyword in (?)", words).order("keyword asc")
end
def glossary?
not glossary.empty?
end
def lock_for(language)
Tr8n::TranslationKeyLock.for(self, language)
end
def lock!(language = Tr8n::Config.current_language, translator = Tr8n::Config.current_translator)
lock_for(language).lock!(translator)
end
def unlock!(language = Tr8n::Config.current_language, translator = Tr8n::Config.current_translator)
lock_for(language).unlock!(translator)
end
def unlock_all!
locks.each do |lock|
lock.unlock!
end
end
def locked?(language = Tr8n::Config.current_language)
lock_for(language).locked?
end
def unlocked?(language = Tr8n::Config.current_language)
not locked?(language)
end
def followed?(translator = Tr8n::Config.current_translator)
Tr8n::TranslatorFollowing.following_for(translator, self)
end
def add_translation(label, rules = nil, language = Tr8n::Config.current_language, translator = Tr8n::Config.current_translator)
raise Tr8n::Exception.new("The sentence contains dirty words") unless language.clean_sentence?(label)
translation = Tr8n::Translation.create(:translation_key => self, :language => language,
:translator => translator, :label => label, :rules => rules)
translation.vote!(translator, 1)
translation
end
# returns all translations for the key, language and minimal rank
def translations_for(language = nil, rank = nil)
translations = Tr8n::Translation.where("translation_key_id = ?", self.id)
translations = translations.where("language_id = ?", language.id) if language
translations = translations.where("rank >= ?", rank) if rank
translations.order("rank desc").all
end
# used by the inline popup dialog, we don't want to show blocked translations
def inline_translations_for(language)
translations_for(language, -50)
end
# returns only the translations that meet the minimum rank
def valid_translations_for(language)
Tr8n::Cache.fetch("translations_#{language.locale}_#{key}") do
translations_for(language, Tr8n::Config.translation_threshold)
end
end
def translation_with_such_rules_exist?(language_translations, translator, rules_hash)
language_translations.each do |translation|
return true if translation.matches_rule_definitions?(rules_hash)
end
false
end
# {"actor"=>{"gender"=>"true"}, "target"=>{"gender"=>"true", "value"=>"true"}}
def generate_rule_permutations(language, translator, dependencies)
return if dependencies.blank?
token_rules = {}
dependency_mapping = {}
# make into {"actor"=>[1], "target"=>[1], "target_@1"=>[2]}
dependencies.each do |dependency, rule_types|
rule_types.keys.each_with_index do |rule_type, index|
token_key = dependency + "_@#{index}"
dependency_mapping[token_key] = dependency
rules = language.default_rules_for(rule_type)
token_rules[token_key] = [] unless token_rules[token_key]
token_rules[token_key] << rules
token_rules[token_key].flatten!
end
end
language_translations = translations_for(language)
new_translations = []
token_rules.combinations.each do |combination|
rules = []
rules_hash = {}
combination.each do |token, language_rule|
token_key = dependency_mapping[token]
rules_hash[token_key] ||= []
rules_hash[token_key] << language_rule.id.to_s
end
rules = rules_hash.collect{|token_key, rule_ids| {:token => token_key, :rule_id => rule_ids}}
# if the user has previously create this particular combination, move on...
next if translation_with_such_rules_exist?(language_translations, translator, rules_hash)
new_translations << Tr8n::Translation.create(:translation_key => self, :language => language, :translator => translator, :label => sanitized_label, :rules => rules)
end
new_translations
end
def self.random
self.limit(1).offset(count-1)
end
# returns back grouped by context - used by API
def find_all_valid_translations(translations)
if translations.empty?
return {:id => self.id, :key => self.key, :label => self.label, :original => true}
end
# if the first translation does not depend on any of the context rules
# use it... we don't care about the rest of the rules.
if translations.first.rules_hash.blank?
return {:id => self.id, :key => self.key, :label => translations.first.label}
end
# build a context hash for every kind of context rules combinations
# only the first one in the list should be used
context_hash_matches = {}
valid_translations = []
translations.each do |translation|
context_key = translation.rules_hash || ""
next if context_hash_matches[context_key]
context_hash_matches[context_key] = true
if translation.rules_definitions
valid_translations << {:label => translation.label, :context => translation.rules_definitions.clone}
else
valid_translations << {:label => translation.label}
end
end
# always add the default one at the end, so if none of the rules matched, use the english one
valid_translations << {:label => self.label} unless context_hash_matches[""]
{:id => self.id, :key => self.key, :labels => valid_translations}
end
def find_first_valid_translation(language, token_values)
# find the first translation in the order of the rank that matches the rules
valid_translations_for(language).each do |translation|
return translation if translation.matches_rules?(token_values)
end
nil
end
# language fallback approach
# each language can have a fallback language
def find_first_valid_translation_for_language(language, token_values)
translation = find_first_valid_translation(language, token_values)
return [language, translation] if translation
if Tr8n::Config.enable_fallback_languages?
# recursevily go into the fallback language and look there
# no need to go to the default language - there obviously won't be any translations for it
# unless you really won't to keep the keys in the text, and translate the default language
if language.fallback_language and not language.fallback_language.default?
return find_first_valid_translation_for_language(language.fallback_language, token_values)
end
end
[language, nil]
end
# translator fallback approach
# each translator can have a fallback language, which may have a fallback language
def find_first_valid_translation_for_translator(language, translator, token_values)
translation = find_first_valid_translation(language, token_values)
return [language, translation] if translation
if translator.fallback_language and not translator.fallback_language.default?
return find_first_valid_translation_for_language(translator.fallback_language, token_values)
end
[language, nil]
end
def translate(language = Tr8n::Config.current_language, token_values = {}, options = {})
return find_all_valid_translations(valid_translations_for(language)) if options[:api]
if Tr8n::Config.disabled? or language.default?
return substitute_tokens(label, token_values, options.merge(:fallback => false), language).html_safe
end
if Tr8n::Config.enable_translator_language? and Tr8n::Config.current_user_is_translator?
translation_language, translation = find_first_valid_translation_for_translator(language, Tr8n::Config.current_translator, token_values)
else
translation_language, translation = find_first_valid_translation_for_language(language, token_values)
end
# if you want to present the label in it's sanitized form - for the phrase list
if options[:default_language]
return decorate_translation(language, sanitized_label, translation != nil, options).html_safe
end
if translation
translated_label = substitute_tokens(translation.label, token_values, options, language)
return decorate_translation(language, translated_label, translation != nil, options.merge(:fallback => (translation_language != language))).html_safe
end
# no translation found
translated_label = substitute_tokens(label, token_values, options, Tr8n::Config.default_language)
decorate_translation(language, translated_label, translation != nil, options).html_safe
end
###############################################################
## Substitution and Decoration Related Stuff
###############################################################
# this is done when the translations engine is disabled
def self.substitute_tokens(label, tokens, options = {}, language = Tr8n::Config.default_language)
return label.to_s if options[:skip_substitution]
Tr8n::TranslationKey.new(:label => label.to_s).substitute_tokens(label.to_s, tokens, options, language)
end
def substitute_tokens(translated_label, token_values, options = {}, language = Tr8n::Config.current_language)
processed_label = translated_label.to_s.clone
# substitute all data tokens
Tr8n::TokenizedLabel.new(processed_label).data_tokens.each do |token|
next unless tokenized_label.allowed_token?(token)
processed_label = token.substitute(processed_label, token_values, options, language)
end
# substitute all decoration tokens
Tr8n::TokenizedLabel.new(processed_label).decoration_tokens.each do |token|
next unless tokenized_label.allowed_token?(token)
processed_label = token.substitute(processed_label, token_values, options, language)
end
processed_label
end
# TODO: move all this stuff out of the model to decorators
def default_decoration(language = Tr8n::Config.current_language, options = {})
return label if Tr8n::Config.current_user_is_guest?
return label unless Tr8n::Config.current_user_is_translator?
return label unless translator_permitted_to_translate?
return label if locked?(language)
classes = ['tr8n_translatable']
if valid_translations_for(language).any?
classes << 'tr8n_translated'
else
classes << 'tr8n_not_translated'
end
html = ""
html << sanitized_label
html << ""
html.html_safe
end
def level
return 0 if super.nil?
super
end
def translator_permitted_to_translate?(translator = Tr8n::Config.current_translator)
translator.level >= level
end
def decorate_translation(language, translated_label, translated = true, options = {})
return translated_label if options[:skip_decorations]
return translated_label if Tr8n::Config.current_user_is_guest?
return translated_label unless Tr8n::Config.current_user_is_translator?
return translated_label unless Tr8n::Config.current_translator.enable_inline_translations?
return translated_label unless translator_permitted_to_translate?
return translated_label if locked?(language)
return translated_label if self.language == language
classes = ['tr8n_translatable']
if language.default?
classes << 'tr8n_not_translated'
elsif options[:fallback]
classes << 'tr8n_fallback'
else
classes << (translated ? 'tr8n_translated' : 'tr8n_not_translated')
end
html = ""
html << translated_label
html << ""
html
end
def verify!(time = Time.now)
update_attributes(:verified_at => time)
end
def update_translation_count!
update_attributes(:translation_count => Tr8n::Translation.count(:conditions => ["translation_key_id = ?", self.id]))
end
def source_map
@source_map ||= begin
map = {}
sources.each do |source|
(map[source.domain.name] ||= []) << source
end
map
end
end
def clear_cache
Tr8n::Cache.delete("translation_key_#{key}")
end
def add_translation(label, rules = nil, lang = Tr8n::Config.current_language, translator = Tr8n::Config.current_translator)
Tr8n::Translation.create(:translation_key => self, :label => label, :language => lang, :translator => translator)
end
def to_api_hash
{
:key => self.key,
:label => self.label,
:description => self.description,
:locale => (locale || Tr8n::Config.default_locale),
:translations => translations_for(nil, Tr8n::Config.translation_threshold).collect{|t| t.to_api_hash}
}
end
# create translation key from API hash
def self.create_from_api_hash(tkey_hash, translator, opts = {})
return if tkey_hash[:key].blank? or tkey_hash[:label].blank? or tkey_hash[:locale].blank?
tkey = Tr8n::TranslationKey.find_or_create(tkey_hash[:label], tkey_hash[:description])
# return unless tkey.key==tkey_hash[:key] # need to warn the user that the key methods don't match
opts[:force_create] = Tr8n::Config.synchronization_create_rules? if opts[:force_create].nil?
(tkey_hash[:translations] || []).each do |trn_hash|
Tr8n::Translation.create_from_api_hash(tkey, translator, trn_hash, opts)
end
tkey
end
###############################################################
## Feature Related Stuff
###############################################################
def self.title
"Original Phrase in {language}".translate(nil, :language => Tr8n::Config.current_language.native_name)
end
def self.help_url
'/tr8n/help'
end
def suggestions?
true
end
def rules?
translation_tokens? or Tr8n::Config.current_language.has_rules?
end
def dictionary?
true
end
def sources?
true
end
###############################################################
## Search Related Stuff
###############################################################
def self.filter_phrase_type_options
[["all", "any"],
["without translations", "without"],
["with translations", "with"],
["followed by me", "followed"]
]
end
def self.filter_phrase_status_options
[["any", "any"],
["pending approval", "pending"],
["approved", "approved"]]
end
def self.filter_phrase_lock_options
[["locked and unlocked", "any"],
["locked only", "locked"],
["unlocked only", "unlocked"]]
end
def self.for_params(params)
results = self.where("tr8n_translation_keys.locale <> ? and (level is null or level <= ?)", Tr8n::Config.current_language.locale, Tr8n::Config.current_translator.level)
if Tr8n::Config.enable_caching?
results = results.where("verified_at is not null")
end
unless params[:search].blank?
results = results.where("(tr8n_translation_keys.label like ? or tr8n_translation_keys.description like ?)", "%#{params[:search]}%", "%#{params[:search]}%")
end
# for with and approved, allow user to specify the kinds
if params[:phrase_type] == "with"
results = results.where("tr8n_translation_keys.id in (select tr8n_translations.translation_key_id from tr8n_translations where tr8n_translations.language_id = ?)", Tr8n::Config.current_language.id)
# if approved, ensure that translation key is locked
if params[:phrase_status] == "approved"
results = results.where("tr8n_translation_keys.id in (select tr8n_translation_key_locks.translation_key_id from tr8n_translation_key_locks where tr8n_translation_key_locks.language_id = ? and tr8n_translation_key_locks.locked = ?)", Tr8n::Config.current_language.id, true)
# if approved, ensure that translation key does not have a lock or unlocked
elsif params[:phrase_status] == "pending"
results = results.where("tr8n_translation_keys.id not in (select tr8n_translation_key_locks.translation_key_id from tr8n_translation_key_locks where tr8n_translation_key_locks.language_id = ? and tr8n_translation_key_locks.locked = ?)", Tr8n::Config.current_language.id, true)
end
elsif params[:phrase_type] == "without"
results = results.where("tr8n_translation_keys.id not in (select tr8n_translations.translation_key_id from tr8n_translations where tr8n_translations.language_id = ?)", Tr8n::Config.current_language.id)
elsif params[:phrase_type] == "followed"
results = results.where("tr8n_translation_keys.id in (select tr8n_translator_following.object_id from tr8n_translator_following where tr8n_translator_following.translator_id = ? and tr8n_translator_following.object_type = ?)", Tr8n::Config.current_translator.id, 'Tr8n::TranslationKey')
end
if params[:phrase_lock] == "locked"
results = results.where("tr8n_translation_keys.id in (select tr8n_translation_key_locks.translation_key_id from tr8n_translation_key_locks where tr8n_translation_key_locks.language_id = ? and tr8n_translation_key_locks.locked = ?)", Tr8n::Config.current_language.id, true)
elsif params[:phrase_lock] == "unlocked"
results = results.where("tr8n_translation_keys.id not in (select tr8n_translation_key_locks.translation_key_id from tr8n_translation_key_locks where tr8n_translation_key_locks.language_id = ? and tr8n_translation_key_locks.locked = ?)", Tr8n::Config.current_language.id, true)
end
results
end
end