客户提出了一个需求: 希望在页面上直接编辑修改翻译内容。这是一个非常好的需求,因为页面上可以很直观地反映翻译(国际化)的效果,如果翻译内容不正确或者有缺失,客户则直接在 页面上修改或添加翻译内容, 这种修改效果是非常好的, 很有所见即所得的范儿。接下来我们站在客户的角度上去思考和实现这个需求。

备注: 这个功能的第一实现人是我的一位同事(大川),并且写这篇博客时参考了他的代码,在此表示感谢。

1. 需求分析

  1. 无关的用户不能修改页面上的翻译内容,所以需要分配一个专门的用户去编辑翻译内容。

  2. 在页面上为翻译内容添加一些特殊的标识来区分翻译内容和非翻译内容。

  3. 需要给客户提供一个比较友好的 UI 来查看和编辑翻译内容。

2. 初步设计

对于需求分析中的第 1 点,我们考虑增加一个叫 translator 即翻译者的角色,只有拥有这个角色的用户才能在页面上编辑翻译内容。

对于第 2 点,我们考虑重写 view 视图里的 helper 方法 t, 通过 t 方法在页面中插入翻译标识。

对于第 3 点,我们考虑以弹出框的形式在当前页面弹出一个 form 表单来编辑翻译内容。

心中有文字还不够,我们还要做到心中有图,为此我们还需要将设计以图的形式表现出来:

t

3. 实现细节

有了设计图(blueprint), 我们可以根据上面的设计图来编写我们的实现细节:

  1. 创建 User 和 Role 模型, User 和 Role 通过中间表 roles_users 关联。

  2. 创建 translator 角色, 将此角色赋予给某 user 对象。

  3. 实现 I18n 的 backend, 可以使用 Redis 数据库作为 backend。

  4. 导入初始的翻译数据。

  5. 重写 t 方法: 如果当前登录用户是 translator, 则插入翻译编辑标识,如果当前用户不是 translator, 则不插入翻译编辑标识。

  6. 鼠标移动到带翻译标识的元素上时,出现提示内容: 关闭提示和编辑,提示内容由 qtip2 实现。

  7. 用户点击编辑按钮时弹出编辑表单,表单内容通过 Ajax 从服务器获取, 表单弹出效果由fancybox 实现。

  8. 用户提交编辑后的翻译内容到服务器,服务器更新 Redis 数据库中的翻译内容。

  9. 无论是获取编辑表单还是保存翻译内容,服务端都需要验证当前用户是否是 translator。

我们将以一个 demo 项目为基础开始实现线上编辑翻译内容的功能,这个 demo 项目已经实现了 1, 2, 3, 4 步,我们将重点放在实现 5, 6, 7, 8, 9上。

4. 编写代码

首先克隆 demo starter 代码,git clone git@github.com:baya/food-list-demo-starter.git

这个 demo starter 跑起后的效果如下图所示:

food-demo

4.1 重写 t 方法

ApplicationHelper 里重写 t 方法:

# app/helpers/application_helper.rb

module ApplicationHelper

  def t(key, options = {})
    result = translate(key, options)
	# translator? 方法定义在 User 中
    if defined?(current_user) && current_user && current_user.translator?
      if result.is_a?(Array)
        result.map {|res| build_editable_tag(key, res) }
      else
        build_editable_tag(key, result)
      end
    else
      result
    end
  end

  private

  def build_editable_tag(key, result)
    "<span class=\"editable trans-edit\" id=\"#{key}\" style=\"display:inline;\">#{result}</span>".html_safe
  end

end

重写 t 方法后,我们以 translator 的身份登录系统,可以看到页面添加了 trans-edit 标记.

4.2 增加编辑翻译的提示

首先引入和 qTip2 相关的 js 和 css 文件, 最方便的方法是将 jquery.qtip.min.js 丢到 app/assets/javascripts 目录下, 将 jquery.qtip.min.css 丢到 app/assets/stylesheets 目录下。

然后 qtip 的实现代码如下:

// app/assets/javascripts/site.js

DailyMealSite.initTransQtip = function(){
    $('.trans-edit').each(function(){
	$(this).qtip({
	    content: {
		text: '<a class="trans-edit-fancy" data-key='+this.id+'>Edit translation!</a>',
		title: { text: 'Translation', button: true}
	    },
	    hide: 'unfocus',
	    show: {solo: true},
	    style: { border: { width: 5, radius: 10 },
		     padding: 10, 
		     textAlign: 'center',
		     tip: true, // Give it a speech bubble tip with automatic corner detection
		     name: 'cream' // Style it according to the preset 'cream' style
		   },
	    events: {
		show: function(event, api) {
		    
		}

	    }

	})
    })
}

最后我们在 app/layouts/application.html.erb 中使用 DailyMealSite.initTransQtip 方法

<script>
  $(document).ready(function(){
      DailyMealSite.initSelectLanguage();
   +  DailyMealSite.initTransQtip();
  })
</script>

增加了 qitp 后的效果如图所示:

4.3 弹出编辑表单

首先我们需要在服务端实现表单。

添加路由:

# config/routes.rb

Rails.application.routes.draw do

  get '/translation', to: "translation#edit", as:  :translation
  post '/translation', to: "translation#update", as: :save_translation

end

控制器相关代码:

# app/controllers/translation_controller.rb

class TranslationController < ApplicationController

  before_filter :auth_translator_role

  def edit
    @key = params[:key]
    @locales = ['zh-CN', 'en']
    render layout: false
  end

  def update
    key = params[:key]
    params[:trans_content].each {|locale, value|
        data = {key => ActiveSupport::JSON.decode(value)}
		if data[key].present?
          I18n.backend.store_translations(locale, data, escape: false)
		end
    }
    refer = request.env["HTTP_REFERER"]
    redirect_to refer
  end

  private

  def auth_translator_role
    if current_user && current_user.translator?
    else
      render status: 403, text: I18n.t('forbiden')
    end
  end
  
end

视图相关代码:

<!-- app/views/translation/edit.html.erb -->
<%= form_tag save_translation_path(key: @key), :id => "translation_form" do %>
  <h2>Key: <%= @key %></h2>
  <table>
    <thead>
      <tr>
	<th><%= t('locale')%></th>
	<th><%= t('content')%></th>	
      </tr>
    </thead>
    <tbody>
      <%- @locales.each do |l| %>
	<tr>
	  <td><%= l %></td>
	  <td><%= text_field_tag "trans_content[#{l}]", I18n.t(@key, locale: l, default: '').to_json, style: 'width:300px;' %></td>
	</tr>
      <% end %>
      <tr>
	<td><%= submit_tag "save", id: "save_translation"%></td>
      </tr>
    </tbody>
  </table>
  
<% end %>

接下来我们通过 ajax 获取这个表单。

首先引入 fancybox, 将 fancybox解压后,将其直接丢到 public 目录下。在 layout/application.html.erb 中引入 fancybox,

<!-- app/views/layout/application.html.erb -->
<head>
  <title>FoodListDemo</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application', media: 'all' %>
 + <%= stylesheet_link_tag    '/fancybox/source/jquery.fancybox.css', media: 'all' %>
 + <%= javascript_include_tag '/fancybox/source/jquery.fancybox.pack.js', media: 'all' %>
  <%= csrf_meta_tags %>
</head>

通过 ajax 获取表单的 js 代码:


// app/assets/javascripts/site.js

DailyMealSite.initPopupTrForm = function(){
    $(document).on('click', '.trans-edit-fancy', function(){
	$.fancybox.showLoading();
	$('div.qtip:visible').qtip('hide');
	$.fancybox({
	    type: "ajax",
	    href: "/translation?key=" + $(this).attr("data-key"),
	    width   : 500,
	    autoSize: false,
	    height    : 500,
	    success: function() {
		$.fancybox.hideLoading();
		
	    }
	}); 
	return false;
    })
}

使用 DailyMealSite.initPopupTrForm,

<!-- app/views/layout/application.html.erb -->

<script>
  $(document).ready(function(){
     DailyMealSite.initSelectLanguage();
     DailyMealSite.initTransQtip();
   +  DailyMealSite.initPopupTrForm();
  })
</script>

将上面的代码写好以后,项目的运行效果如下所示:

food-list-end.gif

备注: mac 下制作 gif 图片可以参考链接: https://gist.github.com/dergachev/4627207

demo 的完整代码在此: https://github.com/baya/food-list-demo