From 7132e45a64d029eee3743c4c32cba0bde8b6a1ec Mon Sep 17 00:00:00 2001 From: plowsof <plowsof@protonmail.com> Date: Tue, 9 Jul 2024 21:36:09 +0100 Subject: [PATCH] plugins: local jekyll-multiple-language-plugin --- Gemfile | 1 - Gemfile.lock | 2 - _config.yml | 3 +- _plugins/jekyll-multiple-languages-plugin.rb | 687 +++++++++++++++++++ _plugins/plugin/version.rb | 6 + 5 files changed, 694 insertions(+), 5 deletions(-) create mode 100644 _plugins/jekyll-multiple-languages-plugin.rb create mode 100644 _plugins/plugin/version.rb diff --git a/Gemfile b/Gemfile index 19285c9d..1f50ebe9 100644 --- a/Gemfile +++ b/Gemfile @@ -4,5 +4,4 @@ gem 'jekyll' gem 'jekyll-paginate' gem 'builder' gem 'wdm', '>= 0.1.0' if Gem.win_platform? -gem 'jekyll-multiple-languages-plugin', '= 1.7.0' gem 'jekyll-feed' diff --git a/Gemfile.lock b/Gemfile.lock index 9fd69f18..3741043a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,8 +34,6 @@ GEM webrick (~> 1.7) jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) - jekyll-multiple-languages-plugin (1.7.0) - jekyll (>= 2.0, < 5.0) jekyll-paginate (1.1.0) jekyll-sass-converter (3.0.0) sass-embedded (~> 1.54) diff --git a/_config.yml b/_config.yml index 49318a3d..34b5904b 100644 --- a/_config.yml +++ b/_config.yml @@ -21,7 +21,6 @@ paginate_path: blog/page:num/ plugins: - jekyll-paginate - - jekyll-multiple-languages-plugin - jekyll-feed feed: @@ -29,7 +28,7 @@ feed: # jekyll-multiple-languages-plugin settings: languages: ["en", "es", "it", "pl", "fr", "ar", "ru", "de", "nl", "pt-br", "tr", "zh-cn", "zh-tw", "nb-no", "el"] - +verbose: true exclude_from_localizations: ["img", "css", "fonts", "media", "404.html", "feed.xml", "meta", "_posts", "legal", "blog"] diff --git a/_plugins/jekyll-multiple-languages-plugin.rb b/_plugins/jekyll-multiple-languages-plugin.rb new file mode 100644 index 00000000..229bcdf1 --- /dev/null +++ b/_plugins/jekyll-multiple-languages-plugin.rb @@ -0,0 +1,687 @@ +=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.exists?(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) + + puts "Missing i18n key: #{lang}:#{key}" + puts "Using translation '%s' from default language: %s" %[translation, site.config['default_lang']] + 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 ) \ No newline at end of file diff --git a/_plugins/plugin/version.rb b/_plugins/plugin/version.rb new file mode 100644 index 00000000..0254e6bf --- /dev/null +++ b/_plugins/plugin/version.rb @@ -0,0 +1,6 @@ +module Jekyll + module MultipleLanguagesPlugin + VERSION = "1.8.0" + end +end +