Laravel md文档使用 GoogleTranslate 翻译分享

简介

纯机翻,每个版本初略的过了一遍,属于能看的状态,本文只分享翻译过程,翻译结果可能还需要优化

线上地址

使用到的包

翻译的逻辑

  • 1.将md文档中的不需要翻译的内容先替换掉,例如 自定义样式块代码块链接等,使用emoji表情+序号替换,好处是调用api时会忽略翻译,使用其它字符可能出现问题

  • 2.配置不翻译的单词例如Larvel,Mysql,Apache

  • 3.配置翻译错误的单词,指定自定义翻译后的内容,有些专业单词翻译后总是不对,可以在这里配置

  • 4.GoogleTranslate翻译文本有长度限制,这里我们每次翻译20行,循环翻译完一篇文档

  • 5.最后将 1,2,3三个步骤中替换的内容重新替换回来就完成了翻译单个文件

翻译文档流程

  • 在 github laravel/docs项目中可以下载某个版本的zip包

  • 创建某个版本的数据表模型

  • 写一个Command调用DocService翻译指定版本,等待翻译完成。13.x大概需要30分钟

代码示例

运行截图

截图

调用


#翻译某个文档的标题

php artisan echoyl:helper --translate 文档id 1 13.x(默认13.x)

#翻译某个文档

php artisan echoyl:helper --translate 文档id 0 13.x(默认13.x)

#翻译多个文档

php artisan echoyl:helper --translate 文档id,文档id2... 0 13.x(默认13.x)

#翻译整个 13.x版本

php artisan echoyl:helper --translate init 0 13.x

#翻译多个版本 13.x,12.x,11.x 版本

php artisan echoyl:helper --translate init 0 13.x,12.x,11.x

代码

Command.php


<?php

namespace App\Console\Commands;

use App\Services\echoyl\DocService;

use Illuminate\Console\Command;

class HelperxCommand extends Command

{

 /**

     * The name and signature of the console command.

     *

     * @var string

     */

 // protected $signature = 'sa:command {name} {type=list_0_0}';

 protected  $signature = 'echoyl:helper {params} {justTitle=0} {version=0}

                    {--translate : translate the doc of laravel}';

 /**

     * The console command description.

     *

     * @var string

     */

 protected  $description = 'some helper commands';

 /**

     * Execute the console command.

     *

     * @return mixed

     */

 public  function handle()

    {

 if ($this->option('translate')) {

 $this->translate();

        }

    }

 public  function translate()

    {

 $params = $this->argument('params');

 $justTitle = $this->argument('justTitle');

 $version = $this->argument('version');

 $versions = $version ? explode(',', $version) : [''];

 // d($versions);

 foreach ($versions  as  $version) {

 $ds = new DocService($version ?: '', $this->components);

 if ($params == 'init') {

 $ds->init();

} else {

 $ids = $params ? explode(',', $params) : [];

 foreach ($ids  as  $id) {

 $ds->translateItem($id, $justTitle);

                }

            }

        }

 // d($justTitle, $version, $params);

    }

}

DocService.php


<?php

namespace App\Services\echoyl;

use App\Models\echoyl\doc\Laravel;

use App\Models\echoyl\doc\Laravel11;

use App\Models\echoyl\doc\Laravel12;

use App\Models\echoyl\doc\Laravel13;

use App\Models\echoyl\doc\Laravel42;

use App\Models\echoyl\doc\Laravel58;

use App\Models\echoyl\doc\Laravel6;

use App\Models\echoyl\doc\Laravel7;

use App\Models\echoyl\doc\Laravel8;

use App\Models\echoyl\doc\Laravel9;

use Illuminate\Support\Arr;

use Illuminate\Support\Facades\File;

// use Rct567\DomQuery\DomQuery;

use Stichoza\GoogleTranslate\GoogleTranslate;

use ZipArchive;

class DocService

{

 public  $version = '13.x';

 public  $components = null; // console 消息显示组件

 /**

     * 通过laravel官网获取目录,现在已经废弃

     *

     * @var array

     */

 public  $urls = [

 '9.x' => 'https://laravel.com/docs/9.x',

 '10.x' => 'https://laravel.com/docs/10.x',

 '11.x' => 'https://laravel.com/docs/11.x',

 '12.x' => 'https://laravel.com/docs/12.x',

 '13.x' => 'https://laravel.com/docs/13.x',

    ];

 public  $info_prefix = ''; // 运行时设置info log的前缀显示以查看当前的进度

 public  $all_count = 0; // 总共要翻译的数量

 public  $now_count = 0; // 当前翻译的数量

 public  $now_title = ''; // 当前翻译的标题

 public  $now_name = ''; // 当前翻译的name

 public  $start_time = 0; // 开始时间

 /**

     * 是否翻译代码块的注释信息

     * 注释是单行的所以每行都请求一次,导致速度很慢

     *

     * @var bool

     */

 public  $is_transalte_explain_code = false;

 public  function  __construct($version = '', $components = null)

    {

 if ($version) {

 $this->version = $version;

        }

 if ($components) {

 $this->components = $components;

        }

    }

 // 不翻译的

 public  $dont_trans = [

 'Eloquent', 'Collections', 'Contracts', 'Contract',  'Redis', 'Packages', 'Mocking', 'Facades', 'Facade', 'Blade', 'Helpers', 'Breeze', 'Cashier', 'Stripe', 'Paddle',

 'Dusk', 'Envoy', 'Fortify', 'Folio', 'Homestead', 'Horizon', 'Jetstream', 'Mix', 'Octane', 'Passport', 'Pennant', 'Pint', 'Precognition', 'Prompts',

 'Pulse', 'Sail', 'Sanctum', 'Scout', 'Socialite', 'Telescope', 'Valet', 'Digging', 'Deeper', 'Artisan', 'laravel', 'Laravel', 'Arr',

 'Pipeline', 'Lottery', 'Lotteries', 'Sleep', 'Timebox', 'true', 'false', 'throws', 'throw', 'Fluent',

 'Jobs', 'Job', 'Classes', 'Unique', 'Guzzle', 'Art', 'Boost', 'Echo', 'Reverb', 'Skills', 'Filament', 'Pest',

 'Vendor', 'Listeners', 'Providers', 'Provider', 'Inertia', 'Svelte', 'React', 'Tinker', 'Tables', 'FrankenPHP', 'Franken', 'Tokenizer',

 'Forge', 'Bus', 'Cookie', 'Crypt', 'Gate', 'Session', 'Vite', 'URL', 'Composers',

 'Ably', 'Dehydrating', 'Hydrated', 'Gates', 'Predis', 'AI', 'MCP', 'Travis', 'Chipper', 'Slack', 'Discord', 'Telegram', 'Passkeys',

 'VirtualBox', 'Ubuntu', 'lmm', 'Composer', 'Docker', 'Node', 'Memcached', 'Beanstalkd', 'Mailpit', 'avahi', 'ngrok', 'Xdebug',

 'Apache', 'Supervisor', 'Supervisors', 'RoadRunner', 'Tokens', 'Token', 'Valkey', 'Meilisearch', 'Typesense', 'Algolia', 'Carbon', 'Herd', 'Agentic',

 'Beanstalk', 'Spark', 'Monolog',

    ];

 // 指定翻译

 public  $trans_to = [

 'Event' => '事件',

 'Events' => '事件',

 'Response' => '响应',

 'Responses' => '响应',

 'Headers' => '头信息',

 'Faking' => '伪造',

 'Fakes' => '伪造',

 'Requests' => '请求',

 'Request' => '请求',

 'Application' => '应用',

 'Branch' => '分支',

 'Class' => '类',

 'classes' => '类',

 'Classes' => '类',

 'Arguments' => '参数',

 'Routes' => '路由',

 'Route' => '路由',

 'Routing' => '路由',

 'Injection' => '注入',

 'Resolving' => '解析',

 'Context' => '上下文',

 'Exceptions' => '异常',

 'Lang' => '语言',

 'Schedule' => '计划任务',

 'Views' => '视图',

 'View' => '视图',

 'Streams' => '流',

 'Streaming' => '流',

 'Flashed' => '刷新',

 'Templates' => '模板',

 'Bundling' => '打包',

 'Assets' => '资源',

 'Asset' => '资源',

 'Generation' => '生成',

 'Logging' => '日志',

 'Pusher' => '推送',

 // 'Broadcasting' => '广播',

 'Broadcast' => '广播',

 'Channels' => '频道',

 'Channel' => '频道',

 'Drivers' => '驱动',

 'Driver' => '驱动',

 'Queue' => '队列',

 'Queues' => '队列',

 'Queueable' => '队列',

 'Queued' => '队列',

 'Queueing' => '队列',

 'Listening' => '监听',

 'Models' => '模型',

 'Model' => '模型',

 'Lifetime' => '生命周期',

 'Lifetimes' => '生命周期',

 'Lazy' => '惰性',

 'Lazily' => '惰性',

 'Collection' => '集合',

 'Reference' => '引用',

 'Writing' => '编写',

 'Registering' => '注册',

 'Registration' => '注册',

 'Transports' => '传输',

 'Package' => '包',

 'Packages' => '包',

 'Strings' => '字符串',

 'Hashing' => '哈希',

 'Running' => '运行',

 'Statements' => '语句',

 'Statement' => '语句',

 'Transactions' => '事务',

 'transaction' => '事务',

 'General' => '通用',

 'Unprepared' => '未绑定',

 'Joins' => '连接',

 'Unions' => '联合',

 'Ordering' => '排序',

 'Expressions' => '表达式',

 'Squashing' => '压缩',

 'Seeding' => '种子',

 'Pipelining' => '管道',

 'Relationships' => '关联',

 'Mutators' => '修改器',

 'Cast' => '转换',

 'Casts' => '转换',

 'Casting' => '转换',

 'Castables' => '可转换的',

 'Serialization' => '序列化',

 'Agents' => '代理',

 'Agent' => '代理',

 'Stores' => '存储',

 'Extending' => '扩展',

 'Cookies' => 'Cookie',

 'Pages' => '页面',

 'Hooks' => '钩子',

 'Silenced' => '静默',

 'Workers' => '工作线程',

 'Clients' => '客户端',

 'Recorders' => '记录器',

 'Performance' => '性能',

 'Reloading' => '重载',

 // 'Getting Started' => '入门',

    ];

 public  function info($msg = '')

    {

 if ($this->components && $msg) {

 $prefix = '版本:'.$this->version.' ';

 $prefix .= $this->all_count > 0 ? '第'.$this->now_count.' / '.$this->all_count.'个,当前:'.$this->now_title.' - ' : '';

 $sufix = '';

 if ($this->start_time) {

 $end = microtime(true);

 $time = round($end - $this->start_time, 2);

 $sufix = ' - 耗时:'.$time.'s';

            }

 $this->components->info($prefix.$msg.$sufix);

        }

    }

 public  function getModel($version = '')

    {

 $version = $version ?: $this->version;

 $models = [

 '4.2' => Laravel42::class,

 '5.8' => Laravel58::class,

 '6.x' => Laravel6::class,

 '7.x' => Laravel7::class,

 '8.x' => Laravel8::class,

 '9.x' => Laravel9::class,

 '10.x' => Laravel::class,

 '11.x' => Laravel11::class,

 '12.x' => Laravel12::class,

 '13.x' => Laravel13::class,

        ];

 $class = Arr::get($models, $version);

 if ($class) {

 return  new  $class;

        }

 return  null;

    }

 public  function getDirFromDocumentation()

    {

 $documentation = $this->getMDFromZip('documentation');

 if (! $documentation) {

 $this->info('获取文档失败');

 return;

        }

 $result = [];

 $lines = explode("\n", $documentation);

 // 记录当前一级元素在 $result 中的索引位置

 $currentIndex = -1;

 foreach ($lines  as  $line) {

 // 1. 检测行首是否有空白符(判断是否为二级子菜单)

 $isChild = preg_match('/^\s+/', $line);

 $trimmedLine = trim($line);

 if (empty($trimmedLine)) {

 continue;

            }

 // 2. 匹配内容格式:[标题](链接)

 if (preg_match('/^-\s+\[([^\]]+)\]\(([^)]+)\)/', $trimmedLine, $matches)) {

 $title = trim($matches[1]);

 $link = trim($matches[2]);

 // 初始化节点数据

 $node = ['title' => $title];

 // 检测是否包含 http:// 或 https://

 if (preg_match('/^https?:\/\//', $link)) {

 $node['link'] = $link;

} else {

 $node['name'] = basename($link);

                }

 if ($isChild) {

 // 二级子菜单:直接通过索引将子节点追加到对应的一级元素中

 if ($currentIndex !== -1) {

 $result[$currentIndex]['children'][] = $node;

                    }

} else {

 // 一级元素:推入结果数组,并更新当前索引

 $result[] = $node;

 $currentIndex = count($result) - 1;

                }

            }

 // 3. 匹配内容格式:## 标题 (仅限无缩进的一级分类)

 elseif (! $isChild && preg_match('/^-\s+##\s+(.+)$/', $trimmedLine, $matches)) {

 $result[] = [

 'title' => trim($matches[1]),

 'children' => [],

                ];

 // 更新当前索引

 $currentIndex = count($result) - 1;

            }

        }

 return  $result;

    }

 /**

     * 通过访问laravel文档官网使用domQuery获取目录信息. 现在已废弃

     *

     * @return void

     */

 // public function getDir()

 // {

 //     $url = $this->urls[$this->version];

 //     $file_name = storage_path('app/public/docfile/'.$this->version.'.txt');

 //     if (! file_exists($file_name)) {

 //         $content = file_get_contents($url);

 //         File::put($file_name, $content);

 //     } else {

 //         $content = file_get_contents($file_name);

 //     }

 //     $dom = new DomQuery($content);

 //     $sider = $dom->find('.docs_sidebar:first > ul')->children('li');

 //     $lis = [];

 //     foreach ($sider as $li) {

 //         $title = $li->find('h2')->text();

 //         if (! $title) {

 //             continue;

 //         }

 //         $row = [

 //             'title' => $title,

 //             'name' => $title,

 //             'children' => [],

 //         ];

 //         $children = [];

 //         $second = $li->find('ul')->children('li');

 //         foreach ($second as $sli) {

 //             $title = $sli->find('a')->text();

 //             $name = $sli->find('a')->attr('href');

 //             $children[] = [

 //                 'title' => $title,

 //                 'name' => str_replace('/docs/'.$this->version.'/', '', $name),

 //             ];

 //         }

 //         $row['children'] = $children;

 //         $lis[] = $row;

 //     }

 //     return $lis;

 // }

 public  function init()

    {

 $this->start_time = microtime(true);

 $this->initOriginDirectory();

 $this->translate();

    }

 public  function initFieldName()

    {

 $model = $this->getModel();

 // $directory = $this->getDir();

 $directory = $this->getDirFromDocumentation(); // 直接读取文档中的documentation.md文件获取目录信息

 $start_id = 231;

 foreach ($directory  as  $dir) {

 $item = ['name' => Arr::get($dir, 'name', Arr::get($dir, 'title'))];

 $model->where(['id' => $start_id++])->update($item);

 $model->where(['id' => $start_id++])->update($item);

 $children = Arr::get($dir, 'children', []);

 foreach ($children  as  $child) {

 $item = ['name' => $child['name']];

 $model->where(['id' => $start_id++])->update($item);

 $model->where(['id' => $start_id++])->update($item);

            }

        }

    }

 public  function getItem($item = [], $parent_id = 0)

    {

 $title = Arr::get($item, 'title');

 $name = Arr::get($item, 'name', $title);

 $link = Arr::get($item, 'link', '');

 return [

 'title' => $title, 'name' => $name, 'link' => $link, 'parent_id' => $parent_id,

        ];

    }

 public  function initOriginDirectory()

    {

 $this->info('初始化文档目录');

 // $directory = $this->getDir();

 $directory = $this->getDirFromDocumentation(); // 直接读取文档中的documentation.md文件获取目录信息

 // d($directory);

 $top_id = 2;

 $top_cn_id = 1;

 $model = $this->getModel();

 if (! $model) {

 $this->info('模型不存在');

 return  false;

        }

 $model->where([['id', '>', 0]])->delete();

 $en = [

 'id' => $top_id, 'title' => '英文', 'name' => 'en-us', 'state' => 1,

        ];

 $cn = [

 'id' => $top_cn_id, 'title' => '中文', 'name' => 'zh-cn', 'state' => 1,

        ];

 $model->insert($en);

 $model->insert($cn);

 $count = 0;

 foreach ($directory  as  $dir) {

 $children = Arr::get($dir, 'children', []);

 $item_id = $model->insertGetId($this->getItem($dir, $top_id));

 $item_cn_id = $model->insertGetId($this->getItem($dir, $top_cn_id));

 $count += 1;

 foreach ($children  as  $child) {

 $item = $this->getItem($child, $item_id);

 $item['content'] = $this->getMDFromZip($item['name']);

 $model->insert($item);

 $item['parent_id'] = $item_cn_id;

 $model->insert($item);

 $count += 1;

            }

        }

 $this->all_count = $count;

 $this->info('初始化文档目录成功,共计:'.$count.'条');

    }

 public  function translate($parent_id = 1)

    {

        set_time_limit(0);

 $model = $this->getModel();

 $cn = $model->where(['parent_id' => $parent_id])->get()->toArray();

 foreach ($cn  as  $val) {

 $this->now_title = $val['title'];

 $this->now_name = $val['name'];

 $this->now_count++;

 $title = $this->translateTitle($val['title']);

 $update = [

 'title' => $title,

 'is_translated' => 1,

            ];

 if ($parent_id == 1) {

 $this->translate($val['id']);

} else {

 // 翻译内容

 $update['content'] = $this->translateContent($val['content']);

            }

 $model->where(['id' => $val['id']])->update($update);

        }

    }

 public  function translateItem($id, $just_title = false)

    {

 // 记录开始事件

 $start = microtime(true);

 $model = $this->getModel();

 if (! $model) {

 $this->info('模型错误');

 return [1, '模型错误'];

        }

 $data = $model->where(['id' => $id])->first();

 if (! $data) {

 $this->info('数据错误,id:'.$id);

 return [1, '数据错误'];

        }

 // 获取另一条英文记录

 $en = $model->where(['name' => $data['name']])->where([['id', '<', $data['id']]])->orderBy('id', 'desc')->first();

 if (! $en) {

 $this->info($en['name'].'无英文数据跳过');

 return [1, '无英文数据'];

        }

 $en = $en->toArray();

 $this->now_title = $en['title'];

 $this->now_name = $en['name'];

 $update = [

 'is_translated' => 1,

 'title' => $this->translateTitle($en['title']),

        ];

 if ($en['content'] && ! $just_title) {

 $update['content'] = $this->translateContent($en['content']);

        }

 $model->where(['id' => $data['id']])->update($update);

 // 计算翻译时间

 $end = microtime(true);

 $time = round($end - $start, 2);

 $this->info('翻译成功:'.$data['name'].' 翻译耗时:'.$time.'s');

 return [0, '翻译成功 '.$data['name']];

    }

 /**

     * 过滤设定的不翻译的单词

     *

     * @param  string  $md  文档内容

     * @param  string  $type  encode:编码 decode:解码

     * @return string

     */

 public  function dontTranslateWords($md = '', $type = 'encode')

    {

 $dont_trans = $this->dont_trans;

 $trans_to_keys = array_keys($this->trans_to);

 // php 去重

 $all_trans = array_unique(array_merge($dont_trans, $trans_to_keys));

 // 将$all_trans按字符长度倒叙排列

 $all_trans = collect($all_trans)->sortByDesc(fn ($item) => strlen($item))->toArray();

 foreach ($all_trans  as  $dt) {

 if ($type == 'decode' && isset($this->trans_to[$dt])) {

 // $md = str_replace($dt, ':'.$dt, $md);

 continue;

            }

 $lowword = strtolower($dt);

 $ucfirt = ucfirst($lowword);

 $encode = [' '.$lowword, ' '.$ucfirt, ' '.$dt];

 $decode = [':'.$lowword, ':'.$ucfirt, ':'.$dt];

 if ($type == 'encode') {

 // 匹配开头不能是:=&#\-和其它字母的

 $md = preg_replace([

 '/(?<![:\/\\\\=&#\-\.])\b'.$lowword.'(?![\/\\\\=\-])\b/',

 '/(?<![:\/\\\\=&#\-\.])\b'.$ucfirt.'(?![\/\\\\=\-])\b/',

 '/(?<![:\/\\\\=&#\-\.])\b'.$dt.'(?![\/\\\\=\-])\b/',

], $decode, $md);

 // $md = preg_replace(['/(?<!:)'.$lowword.'/', '/(?<!:)'.$ucfirt.'/'], $decode, $md);

} else {

 $md = str_replace($decode, $encode, $md);

            }

        }

 if ($type == 'decode') {

 // 将翻译后的内容替换回来

 // 将$this->trans_to 按key的长度倒叙排列

 $trans_to = collect($this->trans_to)->sortByDesc(fn ($item, $key) => strlen($key))->toArray();

 foreach ($trans_to  as  $key => $to) {

 $md = preg_replace('/(?<![\/\\\\]) ?:'.$key.' ?(?![\/\\\\])/i', $to, $md);

            }

        }

 return  $md;

    }

 public  function translateToSet($md = '')

    {

 foreach ($this->trans_to as  $key => $to) {

 $md = str_replace($key, $to, $md);

        }

 return  $md;

    }

 public  function translateContent($md = '', $level = 0)

    {

 $this->info('开始翻译内容');

        set_time_limit(0);

 $tr = new GoogleTranslate;

 // 将所有a标签设置为不翻译 preg 正则 dt 占位符 matches 匹配到的内容 index 匹配到需要替换的内容下标 name 匹配的类型

 $pregs = [

            ['preg' => '/<style[^>]*>(.+)<\/style>/isU', 'dt' => '😀', 'matches' => [], 'index' => 0, 'name' => 'style'], // style标签

            ['preg' => '/```((?:(?!```).)+)```/is', 'dt' => '😂', 'matches' => [], 'index' => 0, 'name' => 'code'], // markdown 代码块

            ['preg' => '/(?:\r?\n)((?:[ \t]+[^\r\n]*\r?\n)(?:(?:[ \t]+[^\r\n]*\r?\n)|\r?\n)*)(?=(?:\r?\n(?![ \t])|$))/m', 'dt' => '🫠', 'matches' => [], 'index' => 1, 'name' => 'codeblock'], // 代码块,没有```开始和结束的

            ['preg' => '/(\n\n(    +.*\n*)+\n\s+)[\w\<\>]+/', 'dt' => '😁', 'matches' => [], 'index' => 1, 'name' => 'space'], // 空行包含代码块

            ['preg' => '/<div[^>]*>(.+)<\/div>/isU', 'dt' => '😃', 'matches' => [], 'index' => 0, 'name' => 'div', 'classnames' => [

 'collection-method-list', 'software-list', 'valet-support',

]], // div标签

            ['preg' => '/<p[^>]*>(.*?)<\/p>/isU', 'dt' => '😄', 'matches' => [], 'index' => 0, 'name' => 'p'], // p标签

            ['preg' => '/\(#([^#\s]+)\)/i', 'dt' => '😅', 'matches' => [], 'index' => 0, 'name' => '#'], // #标签

            ['preg' => '/<a[^>]*>(.*?)<\/a>/i', 'dt' => '😆', 'matches' => [], 'index' => 0, 'name' => 'a'], // a标签

            ['preg' => '/(#+\s)([^\n\r]+)/i', 'dt' => '😇', 'matches' => [], 'index' => 1, 'name' => 'h'], // h标签

            ['preg' => '/<img[^>]*>/i', 'dt' => '😉', 'matches' => [], 'index' => 0, 'name' => 'img'], // img标签

            ['preg' => '/`(.*?)`/i', 'dt' => '😊', 'matches' => [], 'index' => 0, 'name' => 'smallcode'], // smallcode标签

            ['preg' => '/{\.(.*?)}/i', 'dt' => '🙃', 'matches' => [], 'index' => 0, 'name' => 'classname'], // {.classname}

            ['preg' => '/\(data:image\/(.*?);base64,(.*?)\)/i', 'dt' => '🤣', 'matches' => [], 'index' => 0, 'name' => 'base64img'], // base64图片

 // ☺️😍😗😘😙😚🤩🥰🥲😋😛😜😝🤑🤪 不要翻译的使用emoji代替,这样就不会翻译,使用其它字符的话可能被破坏

        ];

 if (request('test') && $level == 0) {

 $md = file_get_contents(storage_path('app/public/docfile/test.txt'));

        }

 $tr = $tr->preserveParameters()->setSource('en')->setTarget('zh-CN');

 foreach ($pregs  as  $k => $preg) {

 $name = $preg['name']; // 匹配的类型

 $index = $preg['index'] ?? 0; // 配置中读取哪段matches

            preg_match_all($preg['preg'], $md, $matches);

 if (in_array($name, ['space', 'h'])) {

 // 重复内容先替换长的 再替换短的

 $_match = array_unique($matches[$index]);

 $_match = collect($_match)->sortByDesc(function ($a) {

 return strlen($a);

                })->toArray();

 $matches[$index] = array_values($_match);

 // d($matches,$preg['preg'],$md);

            }

 if ($name == 'codeblock') {

 // d($matches);

            }

 // 翻译div中的内容

 if ($name == 'div' && ! empty($matches[$index])) {

 $classnames = $preg['classnames'];

 if (in_array($this->now_name, ['controllers', 'configuration'])) {

 // 独立检测某个文件中的某个class不需要翻译

 $classnames[] = 'overflow-auto';

                }

 // d($matches);

 // 需要将代码块中的 // .* 注释内容先翻译一遍

 // 过滤掉 classname是 collection-method-list 的div

 $has_classname = false;

 foreach ($classnames  as  $classname) {

 if (strpos($matches[$index][0], $classname) !== false) {

 $has_classname = true;

 break;

                    }

                }

 if (! $has_classname) {

 $new_matches = []; // 需要替换注释所以这里需要一个新的修改了内容后的matches

 foreach ($matches[1] as  $mt_key => $mt) {

 $mt_translated = $this->translateContent($mt, $level + 1);

 $new_matches[$index][$mt_key] = str_replace($mt, "\r".$mt_translated, $matches[$index][$mt_key]); // 将翻译好的内容替换回div中的内容

                    }

 $preg['new_matches'] = $new_matches;

                }

            }

 if ($name == 'h') {

 // 检测h标签后面的单词如果只是单词那么不翻译,判断的规则是不存在空格字符

 if (! empty($matches)) {

 $new_matches = [$index => []];

 foreach ($matches[$index] as  $key => $match) {

 $new_matches[$index][$key] = $match;

                    }

 $need_use_new_match = false;

 $has_count = count($new_matches[$index]);

 // 将$matches[2] 按找字符长度降序排列

 $matches[2] = collect($matches[2])->sortByDesc(function ($a) {

 return strlen($a);

                    })->toArray();

 foreach ($matches[2] as  $key => $match) {

 // 如果没有空格符号且首字母不是大写,则不翻译

 if (preg_match('/^[^A-Z\s][^\s]*$/', $match)) {

 $md = str_replace($match, implode('', [$preg['dt'], $key + $has_count, $preg['dt']]), $md);

 $new_matches[$index][$key + $has_count] = $match;

 $need_use_new_match = true;

                        }

                    }

 if ($need_use_new_match) {

 $preg['new_matches'] = $new_matches;

                    }

                }

 // d($matches, $new_matches, $md);

            }

 if ($name == 'code' && $this->is_transalte_explain_code) {

 // 需要将代码块中的 // .* 注释内容先翻译一遍

 $new_matches = []; // 需要替换注释所以这里需要一个新的修改了内容后的matches

 foreach ($matches[$index] as  $mt_key => $mt) {

                    preg_match_all('/\/\/\s([^\'"\[\]`]+?)\s*$/m', $mt, $_matches);

 // d($_matches);

 if (! empty($_matches) && $_matches[1]) {

 foreach ($_matches[1] as  $_mt) {

 $_m1 = $this->dontTranslateWords($_mt, 'encode');

 $_m1 = $tr->translate($_m1);

 $_m1 = $this->dontTranslateWords($_m1, 'decode');

 $mt = str_replace($_mt, $_m1, $mt);

                        }

                    }

 $new_matches[$index][$mt_key] = $mt;

                }

 $preg['new_matches'] = $new_matches;

            }

 foreach ($matches[$index] as  $key => $match) {

 $md = str_replace($match, implode('', [$preg['dt'], $key, $preg['dt']]), $md);

            }

 $preg['matches'] = $matches;

 $pregs[$k] = $preg;

        }

 $md = $this->dontTranslateWords($md, 'encode');

 if ($level == 0 && request('test')) {

 // d($md);

        }

 $rows = explode("\n", $md);

 // d($rows);

 $big_rows = array_chunk($rows, 20);

 $all_count = count($big_rows);

 $this->info('内容共'.count($rows).'行,分成'.$all_count.'个20行一组,开始翻译');

 // d($big_rows);

 $tred = [];

 foreach ($big_rows  as  $key => $rows) {

 $length = count($rows);

 // if ($key == 7) {

 //     d($rows);

 // }

 if ($rows[0] == "\r" || $rows[0] == '') {

 // 如果最后一个是空行,则翻译后会将空行去掉,所以这里追加一个空行

 $tred[] = "\r";

            }

 $to_be_translated = implode("\n", $rows);

 $tred[] = $tr->translate($to_be_translated);

 $this->info('第'.($key + 1).'/'.$all_count.'组翻译完成'.' 字符串长度:'.strlen($to_be_translated));

 if ($rows[$length - 1] == "\r" || $rows[$length - 1] == '') {

 // 如果最后一个是空行,则翻译后会将空行去掉,所以这里追加一个空行

 $tred[] = "\r";

            }

        }

 $tred = implode("\n", $tred);

 // 有时候[]会翻译回【】中文括号,这里替换回来

 $tred = str_replace(['【', '】'], ['[', ']'], $tred);

 // d($tred);

 $tred = $this->dontTranslateWords($tred, 'decode');

 // d($tred);

 foreach ($pregs  as  $k => $preg) {

 $to_matched = $preg['new_matches'] ?? $preg['matches'];

 foreach ($to_matched[$preg['index']] as  $key => $match) {

 $tred = str_replace(implode('', [$preg['dt'], $key, $preg['dt']]), $match, $tred);

            }

        }

 if ($level == 0 && request('test')) {

            d($tred, $md);

        }

 $this->info('内容翻译完成');

 return  $tred;

    }

 public  function translateTitle($title = '')

    {

 $this->info('翻译标题:'.$title);

 $tr = new GoogleTranslate;

 $titles = explode('/', $title);

 $to_titles = [];

 foreach ($titles  as  $t) {

 $title = $this->dontTranslateWords($t, 'encode');

 $title = $tr->preserveParameters()->setSource('en')->setTarget('zh-CN')->translate($title);

 $title = $this->dontTranslateWords($title, 'decode');

 $to_titles[] = trim($title);

        }

 $title = implode('/', $to_titles);

 $this->info('翻译标题成功:'.$title);

 return  $title;

    }

 public  function getMDFromZip($name)

    {

 $filename = 'docs-'.$this->version;

 $from = storage_path('app/public/docfile/'.$filename.'.zip');

 $to = storage_path('app/public/docfile');

 $dir = storage_path('app/public/docfile/'.$filename);

 if (! is_dir($dir)) {

 $this->unpackZip($from, $to);

        }

 $file = storage_path('app/public/docfile/'.$filename.'/'.$name.'.md');

 if (file_exists($file)) {

 return file_get_contents($file);

        }

 return  '';

    }

 public  function unpackZip($file, $unzip)

    {

 $zip = new ZipArchive;

 if (! file_exists($file)) {

 return  false;

        }

 if ($zip->open($file) === true) {

 $zip->extractTo($unzip);

 $zip->close();

 return  true;

} else {

 return  false;

        }

    }

 /**

     * 通过id

     */

 public  function getDownFiles($parent_id = 0)

    {

        set_time_limit(0);

 $model = $this->getModel();

 $data = $model->where(['parent_id' => $parent_id])->get()->toArray();

 $files = [];

 foreach ($data  as  $val) {

 if ($val['content']) {

 $files[$val['name']] = $val['content'];

            }

 $children = $this->getDownFiles($val['id']);

 if (! empty($children)) {

 $files = array_merge($files, $children);

            }

        }

 return  $files;

    }

 /**

     * 打包zip文件

     *

     * @param [type] $parent_id 需要下载的文档第一个层级id

     * @param [type] $file 自定义文件路径,基于storage_path('app/public/'),默认为docs-版本号_cn.zip

     * @return array

     */

 public  function packZip($parent_id = 1, $file = '')

    {

 $file = $file ?: 'docs-'.$this->version.'_cn.zip';

 $zip = new ZipArchive;

 $filepath = storage_path('app/public/'.$file);

 $fileinfo = pathinfo($filepath);

 $contents = $this->getDownFiles($parent_id);

 if ($zip->open($filepath, ZipArchive::CREATE) === true) {

 foreach ($contents  as  $key => $content) {

 $zip->addFromString($key.'.md', $content);

            }

 $zip->close();

 return [0, ['file' => $file, 'name' => $fileinfo['basename']]];

} else {

 return [1, '创建文件失败'];

        }

    }

}
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 5
JaguarJack

我现在维护的Laravel 中文文档,用 codex 的定时任务每周同步一次,自动维护。很方便的

1周前 评论
echoyl (楼主) 1周前
JaguarJack (作者) 1周前
JaguarJack (作者) 1周前
echoyl (楼主) 1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!