前言

在调研管理本地化资源文件这条路上我一直摸索,之前团队内部也尝试过 Weblate,但是没有推行起来,主要是学习成本太高,并且对于非技术出生的翻译人员来说交互不够友好!

最近在逛 Github 时偶然发现一个 Star 数才三位数的开源项目 Texterify,官网提供 SaaS 服务,体验了以后觉得各方面都还不错,真是深得我心!

介绍

Texterify 的后端是采用 Ruby on Rails 框架开发的,支持的语言文件类型如下:

  • iOS: .strings 文件
  • JSON: .json 文件
  • JSON POEditor: .json 文件
  • JSON FormatJS: .json 文件
  • Chrome JSON: .json 文件
  • go-i18n: .toml 文件
  • TOML: .toml 文件
  • Java: .properties 文件
  • gettext: .po 文件
  • Django: .po 文件
  • Flutter: .arb 文件
  • XLIFF: .xlf, .xliff 文件
  • Rails: .yml, .yaml 文件
  • YAML: .yml, .yaml 文件

虽然是开源的但是 Self-Hosted 版依然需要付费购买 License,否则很多功能用不了,而它的 License 认证使用的是 Gitlab-License 库,所以可以从技术上解决授权的问题。

部署

参考官方文档,使用 Docker Compose 进行 Demo 部署,一路比较顺利,但是如果要自定义一些配置,给项目还有一些待优化的地方,我已经在 Github 上提交了 Issue 和 PR。

# Clone the docker-compose configuration.
git clone https://github.com/texterify/texterify-docker-compose-setup.git
cd texterify-docker-compose-setup

# Generate a secret key for the app.
# Make sure to keep this private.
echo SECRET_KEY_BASE=`openssl rand -hex 64` > secrets.env

# Open the .env file and replace "example.com" with your host (if you
# are trying to run Texterify locally just use "localhost" as host).
# Also make sure to check out the other configuration options (see below).

# Start the service.
docker volume create --name=texterify-database
docker volume create --name=texterify-assets
docker compose up --always-recreate-deps

# After everything has started create the database in another terminal.
docker compose exec app bin/rails db:create db:migrate db:seed

目前遇到的坑如下:

  • 第一个坑是 sidekiq 服务中无法获取到 REDIS_URL 环境变量,我提了 Issue
  • 第二个坑是 SMTP 配置中的 FORM,无法高度自定义,我提了 PR

付费模式

我在本地跑起来 Demo 以后,发现很多功能需要 Upgrade Plan,这个模式类似于 Gitlab CE 那种,具体 Plan 可以参考官网

于是我分析了 Texterify 的源码,发现 License 的许可主要使用的是 Gitlab::License 这个包。

瞟了一眼,发现这中授权模式是通过非对称加密,来加密 License 信息,然后通过公钥来进行解密,获取 License 的信息!

破解

知道了 License 的授权原理,那么接下来就好办了。于是乎我想到了,只要替换 Docker Image 中的 license_key.pub 文件就可以实现对自己生成的 License 进行验证。

准备一个用于生成 License 的脚本文件 license.rb:

require "openssl"
require "gitlab/license"

key_pair = OpenSSL::PKey::RSA.generate(2048)
File.open("license_key", "w") { |f| f.write(key_pair.to_pem) }

public_key = key_pair.public_key
File.open("license_key.pub", "w") { |f| f.write(public_key.to_pem) }

private_key = OpenSSL::PKey::RSA.new File.read("license_key")
Gitlab::License.encryption_key = private_key

license = Gitlab::License.new
license.licensee = {
    "name" => "George",
    "email" => "[email protected]",
    "company" => "Betterde Inc.",
}
license.starts_at = Date.new(2020, 1, 1) # 开始时间
license.expires_at = Date.new(2050, 1, 1) # 结束时间
license.notify_admins_at = Date.new(2049, 12, 1)
license.notify_users_at = Date.new(2049, 12, 1)
license.block_changes_at = Date.new(2050, 1, 1)
license.restrictions = {
    plan: "business", active_users_count: 3000
}

puts "License:"
puts license

data = license.export
puts "Exported license:"
puts data
File.open("texterify.texterify-license", "w") { |f| f.write(data) }

public_key = OpenSSL::PKey::RSA.new File.read("license_key.pub")
Gitlab::License.encryption_key = public_key

data = File.read("texterify.texterify-license")
$license = Gitlab::License.import(data)

puts "Imported license:"
puts $license

unless $license
raise "The license is invalid."
end

if $license.restricted?(:active_user_count)
active_user_count = 10000
if active_user_count > $license.restrictions[:active_user_count]
    raise "The active user count exceeds the allowed amount!"
end
end

if $license.notify_admins?
puts "The license is due to expire on #{$license.expires_at}."
end

if $license.notify_users?
puts "The license is due to expire on #{$license.expires_at}."
end

module Gitlab
class GitAccess
    def check(cmd, changes = nil)
    if $license.block_changes?
        return build_status_object(false, "License expired")
    end
    end
end
end

puts "This instance of Texterify Enterprise Edition is licensed to:"
$license.licensee.each do |key, value|
puts "#{key}: #{value}"
end

if $license.expired?
puts "The license expired on #{$license.expires_at}"
elsif $license.will_expire?
puts "The license will expire on #{$license.expires_at}"
else
puts "The license will never expire."
end

主要需要替换的地方就是 licensee 中的信息,以及过期时间,还有最重要的 license.restrictions。既然要搞就搞最大的,把 plan 设置为 business, active_users_count 设置是限制使用人数的!

然后在 macOS 系统中执行如下命令生成 License:

gem install gitlab-license

ruby license.rb

ls -la
total 32
drwxr-xr-x   6 George  staff   192 Mar  9 15:36 .
drwx------@ 30 George  staff   960 Mar  9 15:36 ..
-rw-r--r--@  1 George  staff  2168 Mar  8 15:57 license.rb
-rw-r--r--   1 George  staff  1679 Mar  8 15:57 license_key
-rw-r--r--   1 George  staff   451 Mar  8 15:57 license_key.pub
-rw-r--r--   1 George  staff  1489 Mar  8 15:57 texterify.texterify-license

然后登录 Texterify 的管理后台,上传 License 就可以了:

Console

这里因为我没有录入 active_users_count 的信息,所以这里没有展示!

I hope this is helpful, Happy hacking…