=begin Jekyll Multiple Languages is an internationalization plugin for Jekyll. It compiles your Jekyll site for one or more languages with a similar approach as Rails does. The different sites will be stored in sub folders with the same name as the language it contains. Please visit https://github.com/screeninteraction/jekyll-multiple-languages-plugin for more details. =end require_relative "plugin/version" module Jekyll #***************************************************************************** # :site, :post_render hook #***************************************************************************** Jekyll::Hooks.register :site, :pre_render do |site, payload| lang = site.config['lang'] end #***************************************************************************** # :site, :post_write hook #***************************************************************************** Jekyll::Hooks.register :site, :post_write do |site| # Moves excluded paths from the default lang subfolder to the root folder #=========================================================================== default_lang = site.config["default_lang"] current_lang = site.config["lang"] exclude_paths = site.config["exclude_from_localizations"] if (default_lang == current_lang && site.config["default_locale_in_subfolder"]) files = Dir.glob(File.join("_site/" + current_lang + "/", "*")) files.each do |file_path| parts = file_path.split('/') f_path = parts[2..-1].join('/') if (f_path == 'base.html') new_path = parts[0] + "/index.html" puts "Moving '" + file_path + "' to '" + new_path + "'" File.rename file_path, new_path else exclude_paths.each do |exclude_path| if (exclude_path == f_path) new_path = parts[0] + "/" + f_path puts "Moving '" + file_path + "' to '" + new_path + "'" if (Dir.exist?(new_path)) FileUtils.rm_r new_path end File.rename file_path, new_path end end end end end #=========================================================================== end Jekyll::Hooks.register :site, :post_render do |site, payload| # Removes all static files that should not be copied to translated sites. #=========================================================================== default_lang = payload["site"]["default_lang"] current_lang = payload["site"][ "lang"] static_files = payload["site"]["static_files"] exclude_paths = payload["site"]["exclude_from_localizations"] default_locale_in_subfolder = site.config["default_locale_in_subfolder"] if default_lang != current_lang static_files.delete_if do |static_file| next true if (static_file.name == 'base.html' && default_locale_in_subfolder) # Remove "/" from beginning of static file relative path if static_file.instance_variable_get(:@relative_path) != nil static_file_r_path = static_file.instance_variable_get(:@relative_path).dup if static_file_r_path static_file_r_path[0] = '' exclude_paths.any? do |exclude_path| Pathname.new(static_file_r_path).descend do |static_file_path| break(true) if (Pathname.new(exclude_path) <=> static_file_path) == 0 end end end end end end #=========================================================================== end #***************************************************************************** # :site, :pre_render hook #***************************************************************************** Jekyll::Hooks.register :site, :pre_render do |site, payload| # Localize front matter data of every page. #=========================================================================== (site.pages + site.documents).each do |item| translate_props(item.data, site) end end ############################################################################## # class Site ############################################################################## class Site attr_accessor :parsed_translations # Hash that stores parsed translations read from YAML files. alias :process_org :process #====================================== # process # # Reads Jekyll and plugin configuration parameters set on _config.yml, sets # main parameters and processes the website for each language. #====================================== def process # Check if plugin settings are set, if not, set a default or quit. #------------------------------------------------------------------------- self.parsed_translations ||= {} self.config['exclude_from_localizations'] ||= [] self.config['default_locale_in_subfolder'] ||= false if ( !self.config['languages'] or self.config['languages'].empty? or !self.config['languages'].all? ) puts 'You must provide at least one language using the "languages" setting on your _config.yml.' exit end # Variables #------------------------------------------------------------------------- # Original Jekyll configurations baseurl_org = self.config[ 'baseurl' ].to_s # Baseurl set on _config.yml dest_org = self.dest # Destination folder where the website is generated # Site building only variables languages = self.config['languages'] # List of languages set on _config.yml # Site wide plugin configurations self.config['default_lang'] = languages.first # Default language (first language of array set on _config.yml) self.config[ 'lang'] = languages.first # Current language being processed self.config['baseurl_root'] = baseurl_org # Baseurl of website root (without the appended language code) self.config['translations'] = self.parsed_translations # Hash that stores parsed translations read from YAML files. Exposes this hash to Liquid. # Build the website for all languages #------------------------------------------------------------------------- # Remove .htaccess file from included files, so it wont show up on translations folders. self.include -= [".htaccess"] #Preload all languages languages.each do |lang| puts "Loading translation from file #{self.config['source']}/_i18n/#{lang}.yml" self.parsed_translations[lang] = YAML.load_file("#{self.config['source']}/_i18n/#{lang}.yml") end languages.each do |lang| # Language specific config/variables if lang != self.config['default_lang'] || self.config['default_locale_in_subfolder'] @dest = dest_org + "/" + lang self.config['baseurl'] = baseurl_org + "/" + lang self.config['lang'] = lang end # Translate site attributes to current language translate_props(self.config, self) puts "Building site for language: \"#{self.config['lang']}\" to: #{self.dest}" process_org end # Revert to initial Jekyll configurations (necessary for regeneration) self.config[ 'baseurl' ] = baseurl_org # Baseurl set on _config.yml @dest = dest_org # Destination folder where the website is generated puts 'Build complete' end if Gem::Version.new(Jekyll::VERSION) < Gem::Version.new("3.0.0") alias :read_posts_org :read_posts #====================================== # read_posts #====================================== def read_posts(dir) translate_posts = !self.config['exclude_from_localizations'].include?("_posts") if dir == '' && translate_posts read_posts("_i18n/#{self.config['lang']}/") else read_posts_org(dir) end end end end ############################################################################## # class PageReader ############################################################################## class PageReader alias :read_org :read #====================================== # read # # Monkey patched this method to remove excluded languages. #====================================== def read(files) read_org(files).reject do |page| page.data['languages'] && !page.data['languages'].include?(site.config['lang']) end end end ############################################################################## # class PostReader ############################################################################## class PostReader if Gem::Version.new(Jekyll::VERSION) >= Gem::Version.new("3.0.0") alias :read_posts_org :read_posts #====================================== # read_posts #====================================== def read_posts(dir) translate_posts = !site.config['exclude_from_localizations'].include?("_posts") if dir == '' && translate_posts read_posts("_i18n/#{site.config['lang']}/") else read_posts_org(dir) end end end end #----------------------------------------------------------------------------- # # Include (with priority—prepend)the translated # permanent link for Page and document # #----------------------------------------------------------------------------- module Permalink #====================================== # permalink #====================================== def permalink return nil if data.nil? || data['permalink'].nil? if site.config['relative_permalinks'] File.join(@dir, data['permalink']) elsif site.config['lang'] # Look if there's a permalink overwrite specified for this lang data['permalink_' + site.config['lang']] || data['permalink'] else data['permalink'] end end end Page.prepend(Permalink) Document.prepend(Permalink) ############################################################################## # class Post ############################################################################## class Post if Gem::Version.new(Jekyll::VERSION) < Gem::Version.new("3.0.0") alias :populate_categories_org :populate_categories #====================================== # populate_categories # # Monkey patched this method to remove unwanted strings # ("_i18n" and language code) that are prepended to posts categories # because of how the multilingual posts are arranged in subfolders. #====================================== def populate_categories categories_from_data = Utils.pluralized_array_from_hash(data, 'category', 'categories') self.categories = ( Array(categories) + categories_from_data ).map {|c| c.to_s.downcase}.flatten.uniq self.categories.delete("_i18n") self.categories.delete(site.config['lang']) return self.categories end end end ############################################################################## # class Document ############################################################################## class Document if Gem::Version.new(Jekyll::VERSION) >= Gem::Version.new("3.0.0") alias :populate_categories_org :populate_categories #====================================== # populate_categories # # Monkey patched this method to remove unwanted strings # ("_i18n" and language code) that are prepended to posts categories # because of how the multilingual posts are arranged in subfolders. #====================================== def populate_categories data['categories'].delete("_i18n") data['categories'].delete(site.config['lang']) merge_data!({ 'categories' => ( Array(data['categories']) + Utils.pluralized_array_from_hash(data, 'category', 'categories') ).map(&:to_s).flatten.uniq }) end end end #----------------------------------------------------------------------------- # # The next classes implements the plugin Liquid Tags and/or Filters # #----------------------------------------------------------------------------- ############################################################################## # class LocalizeTag # # Localization by getting localized text from YAML files. # User must use the "t" or "translate" liquid tags. ############################################################################## class LocalizeTag < Liquid::Tag #====================================== # initialize #====================================== def initialize(tag_name, key, tokens) super @key = key.strip end #====================================== # render #====================================== def render(context) if "#{context[@key]}" != "" # Check for page variable key = "#{context[@key]}" else key = @key end key = Liquid::Template.parse(key).render(context) # Parses and renders some Liquid syntax on arguments (allows expansions) site = context.registers[:site] # Jekyll site object lang = site.config['lang'] translation = site.parsed_translations[lang].access(key) if key.is_a?(String) if translation.nil? or translation.empty? translation = site.parsed_translations[site.config['default_lang']].access(key) if site.config["verbose"] puts "Missing i18n key: #{lang}:#{key}" puts "Using translation '%s' from default language: %s" %[translation, site.config['default_lang']] end end TranslatedString.translate(key, lang, site) translation end end ############################################################################## # class LocalizeInclude # # Localization by including whole files that contain the localization text. # User must use the "tf" or "translate_file" liquid tags. ############################################################################## module Tags class LocalizeInclude < IncludeTag #====================================== # render #====================================== def render(context) if "#{context[@file]}" != "" # Check for page variable file = "#{context[@file]}" else file = @file end file = Liquid::Template.parse(file).render(context) # Parses and renders some Liquid syntax on arguments (allows expansions) site = context.registers[:site] # Jekyll site object default_lang = site.config['default_lang'] validate_file_name(file) includes_dir = File.join(site.source, '_i18n/' + site.config['lang']) # If directory doesn't exist, go to default lang if !Dir.exist?(includes_dir) includes_dir = File.join(site.source, '_i18n/' + default_lang) elsif # If file doesn't exist, go to default lang Dir.chdir(includes_dir) do choices = Dir['**/*'].reject { |x| File.symlink?(x) } if !choices.include?( file) includes_dir = File.join(site.source, '_i18n/' + default_lang) end end end Dir.chdir(includes_dir) do choices = Dir['**/*'].reject { |x| File.symlink?(x) } if choices.include?( file) source = File.read(file) partial = Liquid::Template.parse(source) context.stack do context['include'] = parse_params( context) if @params contents = partial.render(context) ext = File.extname(file) converter = site.converters.find { |c| c.matches(ext) } contents = converter.convert(contents) unless converter.nil? contents end else raise IOError.new "Included file '#{file}' not found in #{includes_dir} directory" end end end end # Override of core Jekyll functionality, to get rid of deprecation # warning. See https://github.com/jekyll/jekyll/pull/7117 for more # details. class PostComparer def initialize(name) @name = name all, @path, @date, @slug = *name.sub(%r!^/!, "").match(MATCHER) unless all raise Jekyll::Errors::InvalidPostNameError, "'#{name}' does not contain valid date and/or title." end escaped_slug = Regexp.escape(slug) @name_regex = %r!_posts/#{path}#{date}-#{escaped_slug}\.[^.]+| ^#{path}_posts/?#{date}-#{escaped_slug}\.[^.]+!x end end end ############################################################################## # class LocalizeLink # # Creates links or permalinks for translated pages. # User must use the "tl" or "translate_link" liquid tags. ############################################################################## class LocalizeLink < Liquid::Tag #====================================== # initialize #====================================== def initialize(tag_name, key, tokens) super @key = key end #====================================== # render #====================================== def render(context) if "#{context[@key]}" != "" # Check for page variable key = "#{context[@key]}" else key = @key end key = Liquid::Template.parse(key).render(context) # Parses and renders some Liquid syntax on arguments (allows expansions) site = context.registers[:site] # Jekyll site object key = key.split namespace = key[0] lang = key[1] || site.config[ 'lang'] default_lang = site.config['default_lang'] baseurl = site.baseurl pages = site.pages url = ""; if default_lang != lang || site.config['default_locale_in_subfolder'] baseurl = baseurl + "/" + lang end collections = site.collections.values.collect{|x| x.docs}.flatten pages = site.pages + collections for p in pages unless p['namespace'].nil? page_namespace = p['namespace'] if namespace == page_namespace permalink = p['permalink_'+lang] || p['permalink'] url = baseurl + permalink end end end url end end end # End module Jekyll ################################################################################ # class Hash ################################################################################ unless Hash.method_defined? :access class Hash #====================================== # access #====================================== def access(path) ret = self path.split('.').each do |p| if p.to_i.to_s == p ret = ret[p.to_i] else ret = ret[p.to_s] || ret[p.to_sym] end break unless ret end ret end end end #====================================== # translate_key # # Translate given key to given language. #====================================== def translate_key(key, lang, site) translation = site.parsed_translations[lang].access(key) if key.is_a?(String) if translation.nil? or translation.empty? translation = site.parsed_translations[site.config['default_lang']].access(key) if translation.nil? or translation.empty? translation = "" if site.config["verbose"] puts "Missing i18n key: #{lang}:#{key}" puts "Using translation '%s' from default language: %s" %[translation, site.config['default_lang']] end end end translation end ################################################################################ # class TranslatedString ################################################################################ class TranslatedString < String #====================================== # initialize #====================================== def initialize(*several_variants, key) super(*several_variants) @key = key end def key @key end #====================================== # translate #====================================== def self.translate(str, lang, site) if str.is_a?(TranslatedString) key = str.key else key = str end return TranslatedString.new(translate_key(key, lang, site), key = key) end end #====================================== # translate_props # # Perform translation of properties defined in translation property list. #====================================== def translate_props(data, site, props_key_name = 'translate_props') lang = site.config['lang'] (data[props_key_name] || []).each do |prop_name| if prop_name.is_a?(String) prop_name = prop_name.strip if prop_name.empty? puts "There is an empty property defined in '#{props_key_name}'" else prop_value = data[prop_name] if prop_value.is_a?(String) and !prop_value.empty? data[prop_name] = TranslatedString.translate(prop_value, lang, site) end end else puts "Incorrect property name '#{prop_name}'. Must be a string" end end end ################################################################################ # Liquid tags definitions Liquid::Template.register_tag('t', Jekyll::LocalizeTag ) Liquid::Template.register_tag('translate', Jekyll::LocalizeTag ) Liquid::Template.register_tag('tf', Jekyll::Tags::LocalizeInclude) Liquid::Template.register_tag('translate_file', Jekyll::Tags::LocalizeInclude) Liquid::Template.register_tag('tl', Jekyll::LocalizeLink ) Liquid::Template.register_tag('translate_link', Jekyll::LocalizeLink )