firewire / fluency
ProcessWire的完整翻译增强套件
Requires
- php: >=8.1.0
- wireframe-framework/processwire-composer-installer: ^1.1.1
This package is auto-updated.
Last update: 2024-09-10 20:55:34 UTC
README
Fluency是一个功能丰富的模块,用于ProcessWire CMS/CMF,它将第三方翻译服务与用户友好的界面集成在一起,以便在任意多语言字段上翻译任何页面上的内容。它还提供了强大的工具,帮助开发者更快地创建多语言网站和应用。
Fluency可用于:
- 在开发新的ProcessWire应用程序时
- 在向现有的ProcessWire应用程序添加多语言功能时
- 向现有的多语言ProcessWire应用程序添加翻译功能
当发现错误时,您可以通过提交Github问题来帮助改进Fluency,或者提交带有修复的拉取请求。
关于升级Fluency的说明 从早期版本升级到1.0.8及之前版本可能会导致错误发生。建议卸载版本1.0.9之前的版本,并安装最新版本。这不会导致任何内容丢失,但是必须重新输入API密钥并重新完成模块配置。
有关Fluency的支持和社区讨论,请访问ProcessWire论坛中的模块主题。
内容
要求
- ProcessWire >= 3.0.2
- PHP >= 8.1
- 模块依赖
- LanguageSupport
- LanguageTabs
- ProcessLanguage
- UIKit管理主题
- 在ProcessWire和Fluency中至少配置了2种语言,以便向字段添加翻译
- 选择第三方翻译服务的API密钥,免费层可用
安装
您可以通过以下三种方法中的任何一种将Fluency安装到您的ProcessWire项目中。
方法1:在ProcessWire中使用“从目录添加模块”和类名Fluency
方法2:通过Composer使用composer require firewire/fluency
方法3:从本存储库或模块目录下载,并将其内容解压缩到/site/modules/
添加模块后,在ProcessWire中安装。
配置
- 打开模块配置页面,选择一个翻译引擎,保存
- 完成翻译引擎设置,保存
- 创建语言关联,保存
- 在适当的地方分配
fluency-translate
权限
这就完成了。所有多语言字段现在都将具有点击翻译按钮和位于管理员菜单栏中的翻译工具。可以配置的语言数量没有限制,并且可以随时添加新语言。
如果ProcessWire中没有语言,或者有语言但没有与Fluency配置,只要存在有效的API密钥且当前用户具有fluency-translate
权限,仍然可以在管理员菜单中使用翻译工具。只有配置了Fluency中的语言,输入字段翻译按钮才会渲染。
翻译引擎
Fluency将第三方翻译服务集成为“翻译引擎”。当你配置Fluency时,你可以选择使用哪个服务。目前可用的翻译服务如下
每个翻译服务都有其优势,但并非所有功能都适用于所有引擎,因为Fluency依赖于每个翻译服务提供的功能,所以请查看每个服务并选择适合您项目的服务。
如果您有兴趣通过构建翻译引擎来为Fluency做出贡献,请参阅下面的贡献。
本地化Fluency
Fluency UI元素的所有文本都可以翻译,包括消息、错误以及用户用于与Fluency交互的元素。这是通过ProcessWire的语言设置完成的。在管理员中访问“设置”->“语言”,并在语言管理页面中的“查找要翻译的文件”功能。
所有可翻译文本都位于Fluency/app/FluencyLocalization.php
升级或移除
将Fluency添加到您的ProcessWire应用程序中、升级或移除Fluency不会影响内容。最多,您可能需要(但不一定)更新或重新配置Fluency模块配置。设置是为每个翻译引擎单独保存的,因此可以在不丢失每个引擎的配置的情况下在引擎之间切换。
在升级Fluency时,请检查Fluency配置页面,以确保您的设置正确,并查看可能已添加的新功能。
查看CHANGELOG.md
文件以获取更改的始终更新的列表。
功能和用法
翻译输入字段
Fluency可以翻译任何页面上任何类型的字段的内容。这包括
- 纯文本区域或文本
- CKEditor内容(常规、内联)
- TinyMCE内容(常规、内联)
- 图片/文件描述
- 页面名称/URL
- 表格
- 重复器/重复器矩阵
Fluency设计得如此之好,以至于任何位置或与其他字段的组合/嵌套中的多语言字段都将可翻译。
当在包含指向其他页面链接的TinyMCE或CKEditor字段中翻译内容时,URL也会自动转换为该语言的链接。
翻译模板和模块
Fluency与ProcessWire的本地翻译页面集成,您可以在其中选择要翻译的文件。这些包括
- 包含字符串的模板被包裹在
__()
函数中 - 核心模块
- 包含字符串的包裹在
__()
函数中的站点模块
在您的ProcessWire管理员中,导航到一个语言页面,使用“查找要翻译的文件按钮”,选择您的文件,并编辑翻译。每个值的字段将有一个按钮,将字段上方的字符串翻译成您当前正在编辑的语言。为了提高速度,请使用顶部的“一键翻译全部”按钮。这将在几秒钟内同时翻译页面上的所有值。
注意:请记住,翻译站点和核心模块可能与您的API使用成本相对较高。建议您确定并优先考虑ProcessWire网站/应用程序中使用的文件/模块。
禁用单个字段的翻译
可能存在某些字段是多语言的,但不需要翻译。这些字段可能需要为每种语言设置不同的值,但包含的内容不适合翻译,例如URL、电话号码、电子邮件地址等。要单独禁用翻译,请转到多语言字段的“详细信息”选项卡,并勾选“禁用此字段的翻译”复选框。这可以在任何时候修改,而不会影响内容或其他功能。
独立翻译器
Fluency还提供一个独立的翻译功能,位于管理菜单栏中的一项,可以从任何页面访问,或者通过点击位于字段下方的翻译按钮旁边的翻译图标来访问。这不需要在Fluency中配置ProcessWire语言,只需配置翻译引擎使用第三方服务即可。
要使用独立翻译器,用户必须被分配fluency-translate
权限。
修改内容指示
当ProcessWire字段中的内容发生变化时,Fluency将在该字段的“语言”选项卡上以斜体显示并添加一条绿色线条。如果内容恢复到原始值,则绿色线条将消失。这使用户在编辑和翻译内容时知道哪些字段已被更改,而无需点击到另一个语言选项卡。这有助于内容输入并有助于防止不同语言之间内容偏差。
缓存
翻译
默认情况下,所有翻译都会在一个月内缓存。这有助于减少API账户使用,因为相同的内容被翻译多次,并显著提高翻译速度。可以在Fluency模块配置页面上开启/关闭缓存。也可以通过模块配置页面、通过Fluency模块API或通过使用Fluency管理REST API的AJAX请求手动清除翻译缓存。
翻译缓存依赖于精确值匹配,包括标点符号、拼写和大小写。这确保始终准确返回精确翻译。包含多个字符串的翻译将被一起缓存。
可翻译的语言
Fluency使用所选第三方翻译服务的识别语言列表来确定在配置和使用Fluency时提供哪些语言。
注意:首次选择/配置翻译引擎时,此信息将永久缓存,直到手动清除,以加快Fluency操作的速度。
每种引擎的语言都分别缓存。这意味着如果翻译服务添加了额外的可翻译语言,必须在模块配置页面上清除此缓存、通过Fluency模块API或通过使用Fluency管理REST API的AJAX请求来清除。
渲染语言标记
Fluency提供工具,帮助快速且容易地构建具有多语言功能的网站。有一些HTML最佳实践有助于提高用户的可访问性并改进SEO性能,其中许多在Fluency中默认可用。
当配置Fluency并使用ProcessWire核心翻译页面翻译模块时,语言名称和ARIA属性将自动本地化。
语言ISO代码
返回第三方服务提供的目标语言ISO代码。
// Get the current language's ISO code $fluency->getLanguageCode(); // => en-us // Get the ISO code for another language using it's ProcessWire ID $fluency->getLanguageCode(1034); // 'de'
提示:使用此功能来指示页面的当前语言,包括在<html>
标签上的lang
属性。
<!doctype html> <html lang="<?= $fluency->getLanguageCode(); ?>"> <!-- markup --> </html>
元链接标签
Fluency可以渲染一个语言列表的<link>
标签,您可以使用这些标签在HTML文档的<head>
中。这有助于用户和搜索引擎找到您页面的所有语言内容。URL将按ProcessWire配置的方式渲染。
要渲染标签
<!doctype html> <html lang="<?= $fluency->getLanguageCode(); ?>"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><?php $page->title; ?></title> <?= $fluency->renderAltLanguageMetaLinkTags(); ?> </head> </html>
输出
<!doctype html> <html lang="en-us"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Your Awesome Website</title> <link rel="alternate" hreflang="https://awesomewebsite.com/" href="x-default" /> <link rel="alternate" hreflang="https://awesomewebsite.com/" href="en-us" /> <link rel="alternate" hreflang="https://awesomewebsite.com/fr/" href="fr" /> <link rel="alternate" hreflang="https://awesomewebsite.com/de/" href="de" /> <link rel="alternate" hreflang="https://awesomewebsite.com/it/" href="it" /> <link rel="alternate" hreflang="https://awesomewebsite.com/es/" href="es" /> </head> </html>
切换页面语言选择元素
您可以轻松渲染一个 <select>
元素,允许用户选择他们当前查看页面的语言。默认情况下,Fluency 还会渲染内联 JavaScript,将页面导航到所选语言,但如果您想自己控制该行为,则可以禁用此功能。如果已翻译并按 ProcessWire 配置,所有文本/标签/值都将使用当前语言渲染。
<div class="language-select"><?= $fluency->renderLanguageSelect() ?></div>
输出
<div class="language-select"> <select id="" class="ft-language-select " aria-label="Select Language" onchange="(function(el){window.location=el.value})(this)" > <option value="/" selected="">English</option> <option value="/fr/">French</option> <option value="/de/">German</option> <option value="/it/">Italian</option> <option value="/es/">Spanish</option> </select> </div>
带有选项
<div class="language-select"> <?= $fluency->renderLanguageSelect( addInlineJs: false, id: 'my-custom-id', classes: ['some', 'classes'] ) ?> </div>
输出
<div class="language-select"> <select id="my-custom-id" class="ft-language-select some classes" aria-label="Select Language"> <option value="/" selected="">English</option> <option value="/fr/">French</option> <option value="/de/">German</option> <option value="/it/">Italian</option> <option value="/es/">Spanish</option> </select> </div>
切换页面语言链接
您还可以渲染一个包含指向当前页面其他语言链接的无序列表。
<div class="languages"><?= $fluency->renderLanguageLinks(); ?></div>
输出
<div class="languages"> <ul> <li class="active"> <a href="/">English</a> </li> <li> <a href="/fr/">French</a> </li> <li> <a href="/de/">German</a> </li> <li> <a href="/it/">Italian</a> </li> <li> <a href="/es/">Spanish</a> </li> </ul> </div>
带有选项
<div class="languages"> <?= $fluency->renderLanguageLinks( id: 'my-language-links', classes: ['my-class'], divider: '|', activeClass: 'current'); ?> </div>
输出
<div class="languages"> <ul id="my-language-links" class="my-class"> <li class="current"> <a href="/">English</a> </li> <li class="divider">|</li> <li> <a href="/fr/">French</a> </li> <li class="divider">|</li> <li> <a href="/de/">German</a> </li> <li class="divider">|</li> <li> <a href="/it/">Italian</a> </li> <li class="divider">|</li> <li> <a href="/es/">Spanish</a> </li> </ul> </div>
程序化使用Fluency
您可以在 ProcessWire 的任何地方使用 $fluency
变量访问 Fluency。以下是一些简单的示例。有关更详细的信息,请查看 Fluency.module.php
中每个方法的 docblocks。
Fluency 在出色的(且我个人推荐的)ProDevTools 模块中的 API 探索工具中完全文档化和格式化。
所有方法都返回不可变且结构特征可预测的 数据传输对象。所有值都可以通过以下方式访问:
- 对象属性
$dtoObject->property
- 可以使用
$dtoObject->toArray()
方法转换为数组 - 可以使用
json_encode($dtoObject)
直接编码为 JSON
数据传输对象还包含用于访问或查找 DTO 内数据的辅助方法
翻译内容
$translation = $fluency->translate( sourceLanguage: 'EN', // Language code used by the translation service targetLanguage: 'DE', // Language code used by the translation service content: 'How do you do fellow developers?', // String or array of strings to translate options: [], // Translation Engine specific options (optional) caching: true // Default is true, false disables, overrides module config ); // => EngineTranslationData // Results may be accessed via properties on the return object, see toArray() example below for all // properties present in the EngineTranslationData object $translation->translations; // => ['Wie geht es Ihnen, liebe Entwickler?'] $translation->fromCache; // => true // $translation->toArray(); Outputs the following: // // array(10) { // 'sourceLanguage' => 'EN' // 'targetLanguage' => 'DE' // 'content' => [ // 'How do you do fellow developers?' // ], // 'translations' => [ // 'Wie geht es Ihnen, liebe Entwickler?' // ], // 'options' => [], // 'fromCache' => true, // 'cacheKey' => 'ff6050ab0d048f7296214bfb21f18af707954dabf4df8013f3f013bc7382a73f', // 'retrievedAt' => '2023-09-17T17:16:25-07:00', // 'error' => NULL, // 'message' => NULL, // } // Get the total number of translated strings returned count($translation); // 1
同时翻译多个字符串
$translation = $fluency->translate(sourceLanguage: 'EN', targetLanguage: 'DE', content: [ 'Must it be?', 'It must be.', ]); // => EngineTranslationData // $translation->toArray(); Outputs the following: // // array(10) { // 'sourceLanguage' => 'EN' // 'targetLanguage' => 'DE' // 'content' => [ // 'Must it be?', // 'It must be.' // ], // 'translations' => [ // 'Muss das sein?', // 'Es muss sein.' // ], // 'options' => [], // 'fromCache' => true, // 'cacheKey' => '18af707954dabf4df8013f3f013bc7382a73fff6050ab0d048f7296214bfb21f', // 'retrievedAt' => '2023-09-17T17:16:25-07:00', // 'error' => NULL, // 'message' => NULL, // } // // Get the total number of translated strings returned count($translation); // 2
获取所有翻译服务语言
此方法返回当前翻译服务识别的所有语言。这包括 Fluency 中未配置的语言。
在调用 $fluency->translate()
时可以使用源/目标语言代码;
$translatableLanguages = $fluency->getTranslatableLanguages(); // => EngineTranslatableLanguagesData // $translatableLanguages->toArray(); Outputs the following: // //array(5) { // ["languages"]=> // array(31) { // [0]=> // object(Fluency\DataTransferObjects\EngineLanguageData)#2667 (7) { // ["sourceName"]=> // string(6) "Danish" // ["sourceCode"]=> // string(2) "DA" // ["targetName"]=> // string(6) "Danish" // ["targetCode"]=> // string(2) "DA" // ["meta"]=> // array(1) { // ["supports_formality"]=> // bool(false) // } // ["error"]=> // NULL // ["message"]=> // NULL // } // [1]=> // object(Fluency\DataTransferObjects\EngineLanguageData)#2668 (7) { // ["sourceName"]=> // string(5) "Dutch" // ["sourceCode"]=> // string(2) "NL" // ["targetName"]=> // string(5) "Dutch" // ["targetCode"]=> // string(2) "NL" // ["meta"]=> // array(1) { // ["supports_formality"]=> // bool(true) // } // ["error"]=> // NULL // ["message"]=> // NULL // } // [2]=> // object(Fluency\DataTransferObjects\EngineLanguageData)#2669 (7) { // ["sourceName"]=> // string(7) "English" // ["sourceCode"]=> // string(2) "EN" // ["targetName"]=> // string(18) "English (American)" // ["targetCode"]=> // string(5) "EN-US" // ["meta"]=> // array(1) { // ["supports_formality"]=> // bool(false) // } // ["error"]=> // NULL // ["message"]=> // NULL // } // // ... ommitted for brevity // } // ["fromCache"]=> // bool(true) // ["retrievedAt"]=> // string(25) "2023-10-27T04:12:12-07:00" // ["error"]=> // NULL // ["message"]=> // NULL // } // Additional methods are available as well, each return their own DTO $danish = $translatableLanguages->bySourceCode('DA'); // => EngineLanguageData for Danish $americanEnglish = $translatableLanguages->byTargetCode('EN-US'); // => EngineLanguageData for English // Get the total number of translatable languages count($translatableLanguages); // => 31
获取所有配置语言
Fluency 为 Fluency 中配置的语言提供强大的数据。在某些情况下,如果不需要修改状态,则可能更愿意使用 ProcessWire 的 $language
对象。
$configuredLanguages = $fluency->getConfiguredLanguages(); // => AllConfiguredLanguagesData // $translatableLanguages->toArray(); Outputs the following: // // array(1) { // ["languages"]=> // array(6) { // [0]=> // object(Fluency\DataTransferObjects\ConfiguredLanguageData)#381 (7) { // ["id"]=> // int(1017) // ["title"]=> // string(7) "English" // ["default"]=> // bool(true) // ["isCurrentLanguage"]=> // bool(true) // ["engineLanguage"]=> // object(Fluency\DataTransferObjects\EngineLanguageData)#380 (7) { // ["sourceName"]=> // string(7) "English" // ["sourceCode"]=> // string(2) "EN" // ["targetName"]=> // string(18) "English (American)" // ["targetCode"]=> // string(5) "EN-US" // ["meta"]=> // array(1) { // ["supports_formality"]=> // bool(false) // } // ["error"]=> // NULL // ["message"]=> // NULL // } // ["error"]=> // NULL // ["message"]=> // NULL // } // [1]=> // object(Fluency\DataTransferObjects\ConfiguredLanguageData)#384 (7) { // ["id"]=> // int(1028) // ["title"]=> // string(6) "French" // ["default"]=> // bool(false) // ["isCurrentLanguage"]=> // bool(false) // ["engineLanguage"]=> // object(Fluency\DataTransferObjects\EngineLanguageData)#383 (7) { // ["sourceName"]=> // string(6) "French" // ["sourceCode"]=> // string(2) "FR" // ["targetName"]=> // string(6) "French" // ["targetCode"]=> // string(2) "FR" // ["meta"]=> // array(1) { // ["supports_formality"]=> // bool(true) // } // ["error"]=> // NULL // ["message"]=> // NULL // } // ["error"]=> // NULL // ["message"]=> // NULL // } // // ...omitted for brevity // } // } // Additional methods are available as well, each return their own DTO. Examples based on the return value shown above: $configuredLanguages->getDefault(); // => ConfiguredLanguageData for English $configuredLanguages->getCurrent(); // => ConfiguredLanguageData for English $configuredLanguages->getByProcessWireId(1028) // => ConfiguredLanguageData for French $configuredLanguages->getBySourceCode('FR') // => ConfiguredLanguageData for French $configuredLanguages->getByTargetCode('EN-US') // => ConfiguredLanguageData for English $configuredLanguages->getBySourceName('French') // => ConfiguredLanguageData for French $configuredLanguages->getByTargetname('English (American)') // => ConfiguredLanguageData for English // Since these return ConfiguredLangaugeData objects, you can access very useful information quickly. $currentLanguage = $configuredLanguages->getCurrent(); // => ConfiguredLanguageData for English $currentLanguage->id; // => 1017 $currentLanguage->title; // => 'English' (Returned localized if translated in ProcessWire) $currentLanguage->default; // => true // All properties under engineLanguage are provided by the Translation Engine // The source/target language codes can be used when calling `$fluency->translate()`; $currentLanguage->engineLanguage->sourceCode; // => 'EN' $currentLanguage->engineLanguage->targetCode; // => 'EN-US' $currentLanguage->engineLanguage->sourceName; // => 'English' $currentLanguage->engineLanguage->targetName; // => 'English (American)' // Get the number of ProcessWire langauges configured in Fluency count($configuredLanguages); // => 6
翻译服务API使用
注意:并非所有翻译服务都提供此信息
$usage = $fluency->getTranslationApiUsage(); // => EngineApiUsageData // $usage->toArray(); Outputs the following: // // array(7) { // ["used"]=> // int(222822) // ["limit"]=> // int(500000) // ["remaining"]=> // int(277178) // ["percentUsed"]=> // string(3) "45%" // ["unit"]=> // string(9) "Character" // ["error"]=> // NULL // ["message"]=> // NULL // }
翻译引擎信息
$engineInfo = $fluency->getTranslationEngineInfo(); // => EngineInfoData // $engineInfo->toArray() outputs the following: // // array(12) { // ["name"]=> // string(5) "DeepL" // ["version"]=> // string(3) "1.1" // ["provider"]=> // string(5) "DeepL" // ["providerApiVersion"]=> // string(3) "2.0" // ["providerApiDocs"]=> // string(31) "https://www.deepl.com/docs-api/" // ["configId"]=> // string(64) "f71ee845229943abd3e5863227d5706600f472b3d31b4de4768878a072b681f1" // ["providesUsageData"]=> // bool(true) // ["details"]=> // string(645) "{Engine description as shown on the Fluency config page}" // ["authorName"]=> // string(8) "FireWire" // ["authorUrl"]=> // string(51) "https://processwire.com/talk/profile/3976-firewire/" // ["error"]=> // NULL // ["message"]=> // NULL // }
管理缓存
// Get the current number of cached translations: $fluency->getCachedTranslationCount(); // Int // Clear the translation cache: $fluency->clearTranslationCache(); // 0 on success // Check if the list of languages translatable by the translation engine are currently cached: $fluency->translatableLanguagesAreCached(); // Bool // Clear the translatable languages cache: $fluency->clearTranslatableLanguagesCache(); // 0 on success
Admin REST API端点
可以通过 ProcessWire 管理端的端点进行 AJAX 调用来与 Fluency 交互。当前用户必须具有 fluency-translate
权限。请参阅 Fluency.module.php
方法 docblocks 以查看接受的方法和响应。基本管理 URL 将与您的 ProcessWire 安装匹配。
$endpoints = $fluency->getApiEndpoints(); // => stdClass // object(stdClass)#376 (6) { // ["endpoints"]=> // string(19) "/processwire/fluency/api/" // ["usage"]=> // string(25) "/processwire/fluency/api/usage/" // ["translation"]=> // string(31) "/processwire/fluency/api/translation/" // ["languages"]=> // string(29) "/processwire/fluency/api/languages/" // ["translationCache"]=> // string(37) "/processwire/fluency/api/cache/translations" // ["translatableLanguagesCache"]=> // string(34) "/processwire/fluency/api/cache/languages" // }
错误处理和日志记录
Fluency 处理与翻译 API 通信时遇到的错误以及 ProcessWire 中遇到的错误。所有数据传输对象都包含 error
和 message
属性。
error
- 将包含在 app/FluencyErrors.php
中定义为类常量的错误类型字符串 message
- 将包含错误类型的人类友好消息。这些位于 app/FluencyLocalization.php
中,并可被翻译
翻译引擎和第三方服务错误存储在 fluency-engine
日志中。如果翻译内容存在问题,请首先查看该日志,以检查翻译服务是否遇到问题。
已知问题
- Grammarly 的浏览器插件可能与 Fluency 冲突,这是许多网络应用中已知的问题。如果您遇到问题,解决方案是在使用 ProcessWire 管理端 Fluency 时禁用 Grammarly,或者在可能未运行 Grammarly 的私密浏览器窗口中登录管理端。Grammarly 提供的说明 在此处。
贡献
模块功能
欢迎提出功能建议。事实上,Fluency 已通过 ProcessWire 社区的出色反馈而变得更好。如果您有建议,请 在 Fluency GitHub 存储库中提交问题。
翻译引擎
Fluency 是模块化的,因为它包含一个框架,可以添加额外的第三方服务作为“翻译引擎”。您可以选择您喜欢的翻译引擎,并提供通过其API连接的凭据。由于此项目是开源的,欢迎为新第三方服务作为翻译引擎的贡献。如果您想请求一个新的第三方服务,请在Fluency GitHub仓库中提交一个问题
如果您想自己开发一个,请随意复制Fluency,构建一个新的翻译引擎,并创建一个拉取请求。
将第三方翻译服务作为翻译引擎集成的开发者文档位于 Fluency/app/Engines/DEVDOC.md
。 文档仍在进行中。如果您需要帮助或更多信息,请随时在ProcessWire 论坛上给我发私信。
成本
Fluency 可免费使用。此模块无需付费,并可用于您用ProcessWire构建的任何网站。此模块作为对杰出的ProcessWire社区的感谢,并与其他模块开发者一起作为贡献,帮助大家构建出色的网站和应用程序。
支持模块开发
模块开发是一项艰巨的工作,也是对ProcessWire生态系统的重要贡献。提供免费和付费模块的开发者花费了大量时间创建和维护我们所有人用于ProcessWire项目的最爱工具。
您可以通过为Github仓库点星、捐赠或通过拉取请求为开发做出贡献来支持模块开发。如果您发现自己离不开某个模块,或者一个模块满足了客户的需求,请考虑捐赠或赞助开发者或他们的项目。
尽管这条信息是代表所有模块开发者向ProcessWire社区发出的,但如果您觉得Fluency很有用,并认为它值得一杯咖啡,您可以通过PayPal给我发一些东西。