Laravel md文档使用 GoogleTranslate 翻译分享
简介
纯机翻,每个版本初略的过了一遍,属于能看的状态,本文只分享翻译过程,翻译结果可能还需要优化
使用到的包
代码中调用
GoogleTranslateapi的封装包 GoogleTranslategoogle翻译一般是被墙的,使用这款修改本地
host,检测延迟最小的ip GoogleTranslateIpCheck
翻译的逻辑
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 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: