译文管理平台

译文管理平台

Author Sure Yu
E-mail yusureyes@163.com

项目介绍

  这是一个用于管理多语言资源的译文管理系统,由于用户分布于全球各地,公司的 App 需要显示多语言,Android 和 iOS 有大量的译文需要管理,手工维护极其麻烦,于是这个系统诞生了,方便 translator 在平台翻译,翻译完成之后,开发者将一键导出代码,直接放置在项目中。

  • 注意 translator 需要自己找人翻译,本系统只是维护译文资源,并不会自动翻译。

    公司 App 下载方式:软件商店搜索 Yeelight

软件架构

PHP 7.1+
Mysql 5.6+
框架: Laravel 5.3
后台系统: iDashboard

安装教程

  1. git clone 项目至本地目录
  2. composer install
  3. cp .env.example .env 修改配置信息
  4. php artisan key:generate 生成 APP_KEY
  5. database/sql 找到 SQL 文件导入数据库
  6. 配置 Apache / Nginx 站点,浏览访问

如果 storage 不可写,请赋权限:
chmod -R 777 storage

演示地址

http://translate.demo.yusure.cn
管理员账号密码: admin 123456
Translator账号密码: translator 123456

使用说明

如何录入源语言(中文)
  1. 创建应用(可以后期创建):可以将多个 Project 分配到一个应用下,因为项目迭代会出现多个 Project,为方便管理,增加应用管理。
  2. 创建项目:点击 Project List,勾选需要翻译的语言,右上角添加项目
  3. 回到 Project List,点击 “录入” 按钮,一个小键盘的图标,录入 key(程序用的) 和 源语言(中文)
如何配置待翻译语言:

修改配置文件config/languages.php

return [
    /* 英语 */
    'en'    => 'English',
    /* 韩语 */
    'ko'    => 'Korean',
    /* 法语 */
    'fr'    => 'French',
];
原文录入完成之后,如何邀请 translator 帮忙翻译:
  1. 首先帮 translator 创建好账号,并发送给他。
  2. 点击查看 Project,在语言管理页面,点击红色的小手图标邀请按钮,将其账号勾选提交。

  3. 在邀请的图标后面是锁定功能,锁定之后,translator 不能修改译文,在 translator 完成翻译之后,该语言的译文自动锁定,如果需要修改,管理员可以随时解锁。
  4. 最后面是给 translator 发送邮件提醒,邮箱是帮 translator 创建账号时添加的,发信配置在 .env 文件。
如何配置对照语言:

例如翻译英文需要参考中文,翻译法语需要英文作为参考,那么就需要修改这个配置文件
config/translator.php

如何导出译文:

当译文都 ready 的时候,需要导出译文,导出译文有两种方式:第一种基于语言去导出,第二种针对整个应用(可以合并多个 Project)可以导出压缩包。
目前可以导出三种格式 Android xml、iOS strings、RN js。

translator 视角
  1. 支持对译文资源进行评论
  2. 支持标记有问题的译文资源,方便后续定位。(注意必须要处理掉所有标记才能完成翻译)

项目截图

本项目在公司内部运行半年有余,经过很多细节优化,为 Android、iOS 工程师提供了便利,现在将其开源出来,为开源事业添砖加瓦!
本项目为开源项目,允许把它用于任何地方,不受任何约束,欢迎 star、 fork 项目。

记一次 Laravel 项目迁移之后 Model 报错问题

  之前迁移过一个 Laravel 5.3 的网站,发布完代码,composer update 之后,能正常访问,随便点了点就再没去管它,后来在后台点击反馈模块就报错,当时在 laravel.log 看到 sql 语句是表名后面没有 s,那肯定报错啊,于是徒手在那个 Model 里面指定上 $table,解决了之后,也就没去深究,后来感觉心里越来越不安,虽然不是我写的,但没去深究,就感觉有罪恶感,于是决定重现这个问题来深入研究一下。

问题现象

数据库有数据表 feedbacks, 对应的 Model 为 Feedback.php 内部没有指定 $table.
在我本地是没有问题的,可以正确指向到 feedbacks 表,于是我从服务器上把代码打了个包,download 到本地重放,果然在本地也报错,可以断言是代码的问题。

代码是这个样子

Feedback.php
<?php 

namespace App\Http\Models;

use Illuminate\Database\Eloquent\Model;

class Feedback extends Model {

    protected $fillable = [];

    protected $dates = [];

    public static $rules = [

    ];

}

报错是这个样子

[2018-03-28 19:59:40] production.ERROR: PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'xxx.feedback' doesn't exist in /www/....../vendor/laravel/framework/src/Illuminate/Database/Connection.php:333

开始排查代码

Step 1. 打印表名

在调用 Feedback 模型之前打印表名出来看看,结果是 feedback 没有 s,报错是肯定的!

$feedbackObj = new Feedback();
$table = $feedbackObj->getTable();
dump( $table );

Step 2. 进入 Model.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php, 跳转到 getTable 方法 从源码很容易看出,如果我在模型里面指定了 $table,会走 if 这块代码直接返回自己设置的表名,如果我没有设置 table,肯定走的下面的自动获取表名逻辑,既然锁定了问题出在自动获取表名这里,就在 return 之前依次打印结果观察。

源码如下:

    /**
     * Get the table associated with the model.
     *
     * @return string
     */
    public function getTable()
    {
        if (isset($this->table)) {
            return $this->table;
        }

        return str_replace('\\', '', Str::snake(Str::plural(class_basename($this))));
    }

打印代码及打印结果如下:

dump( class_basename($this) );    // Feedback
dump( Str::plural( class_basename($this) ) );    // Feedback
dump( Str::snake( Str::plural( class_basename($this) ) ) );    // feedback

从打印结果来看 Str::plural( class_basename($this) ) 这一行已经出现问题了

Step 3. 继续进入 Str.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Support/Str.php, 跳转到 plural 方法

代码很简单,获取英文单词的复数形式,用 Pluralizer 类去调用 plural 静态方法

    /**
     * Get the plural form of an English word.
     *
     * @param  string  $value
     * @param  int     $count
     * @return string
     */
    public static function plural($value, $count = 2)
    {
        return Pluralizer::plural($value, $count);
    }

Step 4. 继续进入 Pluralizer.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Support/Pluralizer.php, 跳转到 plural 方法

if 这段代码不会走,因为 $count 默认是2,feedback 这个单词没有在 $uncountable 这个数组里面出现,两个条件没有一个成立的。
继续打印 $plural,打印结果 Feedback,这里就有问题了。

源码如下:

    /**
     * Get the plural form of an English word.
     *
     * @param  string  $value
     * @param  int     $count
     * @return string
     */
    public static function plural($value, $count = 2)
    {
        if ((int) $count === 1 || static::uncountable($value)) {
            return $value;
        }

        $plural = Inflector::pluralize($value);

        return static::matchCase($plural, $value);
    }

Step 5. 继续进入 Inflector.php 排查

文件路径:/vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php, 跳转到 pluralize 方法。

    /**
     * Returns a word in plural form.
     *
     * @param string $word The word in singular form.
     *
     * @return string The word in plural form.
     */
    public static function pluralize(string $word) : string
    {
        if (isset(self::$cache['pluralize'][$word])) {
            return self::$cache['pluralize'][$word];
        }

        if (!isset(self::$plural['merged']['irregular'])) {
            self::$plural['merged']['irregular'] = self::$plural['irregular'];
        }

        if (!isset(self::$plural['merged']['uninflected'])) {
            self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
        }

        if (!isset(self::$plural['cacheUninflected']) || !isset(self::$plural['cacheIrregular'])) {
            self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')';
            self::$plural['cacheIrregular']   = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')';
        }

        if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) {
            self::$cache['pluralize'][$word] = $regs[1] . $word[0] . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);

            return self::$cache['pluralize'][$word];
        }

        if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
            self::$cache['pluralize'][$word] = $word;

            return $word;
        }

        foreach (self::$plural['rules'] as $rule => $replacement) {
            if (preg_match($rule, $word)) {
                self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);

                return self::$cache['pluralize'][$word];
            }
        }
    }

这个 function 里面 if 判断很多,通过打印锁定在这一行

if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs))

self::$plural['cacheUninflected'] 打印结果里面发现了 feedback 这个单词,原因就是在这里了,但这个单词是怎么来的呢?

"(?:.*[nrlm]ese|.*deer|.*fish|.*measles|.*ois|.*pox|.*sheep|people|cookie|police|.*?media|Amoyese|audio|bison|Borghese|bream|breeches|britches|buffalo|cantus|carp|chassis|clippers|cod|coitus|compensation|Congoese|contretemps|coreopsis|corps|data|debris|deer|diabetes|djinn|education|eland|elk|emoji|equipment|evidence|Faroese|feedback|fish|flounder|Foochowese|Furniture|furniture|gallows|Genevese|Genoese|Gilbertese|gold|headquarters|herpes|hijinks|Hottentotese|information|innings|jackanapes|jedi|Kiplingese|knowledge|Kongoese|love|Lucchese|Luggage|mackerel|Maltese|metadata|mews|moose|mumps|Nankingese|news|nexus|Niasese|nutrition|offspring|Pekingese|Piedmontese|pincers|Pistoiese|plankton|pliers|pokemon|police|Portuguese|proceedings|rabies|rain|rhinoceros|rice|salmon|Sarawakese|scissors|sea[- ]bass|series|Shavese|shears|sheep|siemens|species|staff|swine|traffic|trousers|trout|tuna|us|Vermontese|Wenchowese|wheat|whiting|wildebeest|Yengeese)"

顺着往上找发现在 第三个 if 判断的时候执行了这一行代码,self::$uninflected 这个是关键,马上查找这个变量。

if (!isset(self::$plural['merged']['uninflected'])) {
    self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
}

在 :223 行找到了这个变量的所有值,这个变量的意思是复数是单词原形,不受影响,What?feedback 复数不加 s?顺手查了一下,百度词典,金山词霸很明确的说复数加s,有道词典没有说明,只显示 feedbacks 是名词回馈的意思,通过查了一些资料还是推荐 feedback 为复数形式。
参考资料链接:
http://www.learnenglishwithwill.com/feedback-vs-feedbacks-plural-form/

    /**
     * Words that should not be inflected.
     *
     * @var array
     */
    private static $uninflected = array(
        '.*?media', 'Amoyese', 'audio', 'bison', 'Borghese', 'bream', 'breeches',
        'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'compensation', 'Congoese',
        'contretemps', 'coreopsis', 'corps', 'data', 'debris', 'deer', 'diabetes', 'djinn', 'education', 'eland',
        'elk', 'emoji', 'equipment', 'evidence', 'Faroese', 'feedback', 'fish', 'flounder', 'Foochowese',
        'Furniture', 'furniture', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'gold', 
        'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'jedi',
        'Kiplingese', 'knowledge', 'Kongoese', 'love', 'Lucchese', 'Luggage', 'mackerel', 'Maltese', 'metadata',
        'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'nutrition', 'offspring',
        'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'plankton', 'pliers', 'pokemon', 'police', 'Portuguese',
        'proceedings', 'rabies', 'rain', 'rhinoceros', 'rice', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass',
        'series', 'Shavese', 'shears', 'sheep', 'siemens', 'species', 'staff', 'swine', 'traffic',
        'trousers', 'trout', 'tuna', 'us', 'Vermontese', 'Wenchowese', 'wheat', 'whiting', 'wildebeest', 'Yengeese'
    );

代码找到这里,这个问题就已经明白了,是因为 update 了 doctrine/inflector 这个包导致的。

Step 6. 继续深究

于是重开一个目录,pull 一下这几个版本发现 1.3.0 开始发生了变化,加入了 feedback 没有复数形式。

composer require doctrine/inflector 1.2.0
composer require doctrine/inflector 1.3.0

继续开新目录 Clone 源代码分析:

git clone https://github.com/doctrine/inflector.git 

查看 git log 看到了这个注释信息 Added more uninflected words

探究到这里,我想这个问题真的明白了。

按照惯例得总结一下结尾:

  1. Model 里面尽量指定一个 $table,有可能把握不准单词复数的形式。
  2. composer update 之后要通过 composer.lock 检查有版本变化的包。
  3. 英文真的很重要。
  4. 源码面前,了无秘密。
  5. 祝阅读到最后的人技术再上一个 level。

php 使用 GeoIP 扩展获取 ip 各种信息

项目背景


公司 App 上准备针对客户的ip来推荐最优服务器来快连设备,目前有4个节点,北京、俄勒冈、新加坡、法兰克福。客户端上报 IP,云端根据分配规则返回 server 代号。

资料搜集


一开始打算找第三方的一些API,测试了很多ip定位接口,效果并不好,有的收费,有的需要申请key有调用次数限制,大部分API只有国家和城市的信息,而且不规范,不是国家代码,抓取到结果还需要进一步匹配,因为没有大洲信息,还要自己根据国家去 mapping 大洲。

上面的各种缺点,直接放弃第三方的API,于是找到了 GeoIP 这个扩展,这个有纯PHP版本的,但是我没找到如何获取大洲,直接上C扩展版的,性能肯定没问题,装好扩展,geoip_continent_code_by_name 直接获取大洲简称代码。

php 扩展安装


我的是 Docker 环境 Ubuntu14.04 php5.6, 下面是扩展的安装命令。

apt-get -y --force-yes install php5.6-geoip

注意:--no-install-recommends 这个参数一定不要加,有这个的话安装完成会把 IP 数据包删除的, 我 build docker 镜像的时候吃过亏了,下面是 IP 数据包的目录。

root@1e1931746c6c:/usr/share/GeoIP# ls
GeoIP.dat  GeoIPv6.dat

GeoIP 扩展源码下载: https://pecl.php.net/package/geoip
以下是源码安装步骤:

$ wget https://pecl.php.net/get/geoip-1.1.1.tgz
$ cd geoip-1.1.1
$ phpize
$ ./configure
$ make
$ sudo make install

编写代码


在 phpinfo 能看到 GeoIP,就说明扩展安装好了。
这是 GeoIP 扩展的文档,一个函数获取想要的信息,完全满足了我的需求,比第三方 API 好用的多,直接省去网络请求。
http://php.net/manual/zh/book.geoip.php

Typecho 首页静态化脚本

  哈喽大家好,捣鼓了下博客的优化,把博客主页生成静态文件html了,没有侵入主程序,完全不用担心升级问题,下面分享一下代码。

  1. 在站点根目录下创建或上传 build_index.php,访问这个文件就可以在根目录生成静态文件了。
  2. 更新缓存 http://test.com/build_index.php?password=123456可以在脚本里面设置你的密码,防止被他人利用发起CC攻击,频繁写文件造成服务器IO过高。
  3. 如果不想使用过期更新,可以从脚本里面去掉调用更新那句 script 代码,缓存过期时间修改 $expire 变量。
  4. 另外需要注意的是你的 index.html 要在 index.php 前面,否则不生效。Apache 修改 DirectoryIndex, Nginx 修改 index,IIS 配置默认文档。
<?php
/**
 * 首页静态化脚本
 * Author: Yusure
 * Blog: yusure.cn
 */
ini_set( 'date.timezone', 'PRC' );

/* 缓存过期时间 单位:秒 */
$expire = 86400;
/* 主动刷新密码  格式:http://test.com/build_index.php?password=123456 */
$password = '123456';
$file_time = @filemtime( 'index.html' );
time() - $file_time > $expire && create_index();
isset( $_GET['password'] ) && $_GET['password'] == $password && create_index();

/**
 * 生成 index.html
 */
function create_index()
{
    ob_start();
    include( 'index.php' );
    $content = ob_get_contents();
    $content .= "\n<!-- Create time: " . date( 'Y-m-d H:i:s' ) . " -->";
    /* 调用更新 */
    $content .= "\n<script language=javascript src='build_index.php'></script>";
    ob_clean();
    $res = file_put_contents( 'index.html', $content );
    if ( $res !== false )
    {
        die( 'Create successful' );
    }
    else
    {
        die( 'Create error' );
    }
}

GitHub下载地址:https://gist.github.com/yusureabc/34564707391b6275864b94b3cdc0088f

配置php的session存储到memcache或redis

  PHP默认配置是将session以文件形式存储在服务器上,网站访问量增加之后,单机的io是瓶颈,而且文本读取慢,除了默认的文本还可以存放到数据库,放到内存(memcache,redis)。不建议放到数据库里面,还是配置到内存里面比较爽,既提高了访问速度,又很好的实现了会话共享。

memcache 存储

如何配置

服务端配置很简单只要两条配置命令

  1. 在 php.ini 中全局设置
    session.save_handler = memcache
    session.save_path = "tcp://127.0.0.1:11211"
  2. 单一网站配置(在php入口处添加,用框架的项目只需要在配置文件里面修改就可以了)
    ini_set("session.save_handler", "memcache");
    ini_set("session.save_path", "tcp://192.168.48.128:11211");
如何与网站应用互通

memcached 服务是没有密码的,如果没有限制的暴露在外网,任何用户都是可以连接的。
-l 是监听的服务器IP地址,默认是127.0.0.1,任何ip访问0.0.0.0

- 阅读全文 -