Fluency is a feature-rich module for the ProcessWire CMS/CMF that integrates third party translation services with a user-friendly interface for translating content in any multi-language field on any page. It also provides powerful tools to help developers create multi-language sites and apps faster.
Fluency can be used:
- While developing a new ProcessWire application
- When adding multi-language abilities to an existing ProcessWire application
- To add translation abilities to an existing multi-language ProcessWire application
You can help make Fluency better by filing Github issues when bugs are found, or submit a pull request with fixes.
Notes on upgrading Fluency Upgrading from earlier versions to 1.0.8 and earlier may cause errors to occur. It is recommended that versions prior to 1.0.9 be uninstalled and the latest version installed. This will not cause any content loss however the API key must be re-entered and module configuration be completed again.
For support and community discussion about Fluency, visit the module thread in the ProcessWire forums.
- Requirements
- Installing
- Features and Usage
- Using Fluency Programatically
- Error Handling and Logging
- Known Issues
- Contributing
- Cost
- Supporting Module Development
- ProcessWire >= 3.0.2
- PHP >= 8.1
- Module dependencies
- LanguageSupport
- LanguageTabs
- ProcessLanguage
- UIKit admin theme
- At least 2 languages configured in ProcessWire and Fluency to add translation to fields
- API key for the chosen third party translation service, free tiers are available
You can install Fluency by adding the module to your ProcessWire project using any of the three following methods.
Method 1: Within ProcessWire using 'Add Module From Directory' and the class name Fluency
Method 2: Via Composer with composer require firewire/fluency
Method 3: Download from this repository or the Modules directory and unzip the contents into /site/modules/
After adding the module, install in ProcessWire.
- Open the module configuration page, choose a Translation Engine, save
- Complete the Translation Engine setup, save
- Create language associations, save
- Assign the
fluency-translate
permission where appropriate
That's it. All multi-language fields will now feature click to translate buttons and a translator tool available in the Admin menu bar. There is no limit on how many languages may be configured, and new languages can be added at any time.
If no langauges are present in ProcessWire or if languages are present and not configured with Fluency, it is still possible to use the translator tool in the Admin menu as long as a valid API key is present and the current user has the fluency-translate
permission. Inputfield translation buttons will not render until languages have been configured in Fluency.
Fluency integrates third party translation services as "Translation Engines". When you configure Fluency, you'll choose which service to use. Currently the following translation services are availale:
Each translation service has their strengths, however not all features exist for all engines as Fluency relies on what abilities are available via each translation service, so review each and choose the one that is right for your project.
If you're interested in contributing to Fluency by building a Translation Engine, see Contributing below.
All text for the Fluency UI elements can be translated including messages, errors, and elements users use to interact with Fluency. This is done through ProcessWire's language setup. In the Admin visit Setup->Languages, and the "Find Files To Translate" feature within the language management page.
All translatable texts are located in Fluency/app/FluencyLocalization.php
Adding, upgrading, or removing Fluency from your ProcessWire application will not affect content. At most, you may (but not always) need to update or reconfigure your Fluency module config. Settings are saved individually for each translation engine, so it is possible to switch between engines without losing your configurations for each.
When upgrading Fluency, check the Fluency config page to ensure that your settings are correct and to review any new features that may have been added.
Review the CHANGELOG.md
file for an always up-to-date list of changes.
Fluency can translate content in any type of field on any page. These include:
- Plain textarea or text
- CKEditor content (Regular, Inline)
- TinyMCE content (Regular, Inline)
- Image/file descriptions
- Page names/URLs
- Tables
- Repeater/repeater matrix
Fluency is designed so that any multi-language field in any location or combination/nesting with other fields will be translatable.
When translating content within a TinyMCE or CKEditor field that contains links to other pages, the URLs will also be converted to links in that language automatically.
Fluency integrates with ProcessWire's native translator pages where you can select files to translate. These include:
- Templates that contain strings are wrapped in the
__()
function - Core modules
- Site modules that contain strings wrapped in the
__()
function
In your ProcessWire admin, navigate to a langauge page, use the "Find Files To Translate Button", select your files, and edit the translations. The field for each value will have a button which translates the string above the field to the language you are currently editing. For extra speed, use the "Click To Translate All" button at the top. This will translate all values on the page simultaneously in seconds.
Note: Please keep in mind that translating site and core modules may be relatively expensive operations with respect to your API usage. It is recommended that you identify and prioritize the files/modules that are used in your ProcessWire site/app.
There may be individual fields that are multi-language but translation is not desired. These could be fields that need different values for each language but contain content not suitable for translating such as URLs, phone numbers, email addresses, etc. To disable translation individually, navigate to the Details tab of a multi-language field and check the "Disable translation for this field" checkbox. This can be modified at any time without affecting content or other functionality.
Fluency also provides a standalone translation located as an item in the admin menu bar that can be accessed from any page, or by clicking the translation icon next to the translate buttons located under fields. This does not require that ProcessWire languages are configured in Fluency, only that the Translation Engine is configured to use a third party service.
To use the standalone translator, users must be assigned the fluency-translate
permission.
When content in a ProcessWire field changes, Fluency italicizes and adds a green line on the language tab for that field. If the content is reverted to it's original value, the green line is removed. This lets users who are editing and translating content know which fields have been changed without clicking to another language tab. This assists content input and helps prevent content deviation between languages.
Translations
All translations are cached by default for a period of one month. This helps reduce API account usage where the same content is translated more than once and significantly increases translation speed. Caching can be toggled on/off on the Fluency module config page. The translation cache can also be manually cleared either on the module config page, via the Fluency module API, or via an AJAX request using the Fluency admin REST API.
Translation caching relies on exact value matching including punctuation, spelling, and capitalization. This ensures that an exact translation is always returned accurately. Translations that contain multiple strings are cached together.
Translatable Languages
Fluency uses lists of recognized languages from the selected third party translation service to determine what languages to make available when configuring and using Fluency.
Note: This is cached forever until manually cleared the first time that a translation engine is selected/configured to increase the speed of Fluency's operations.
Available languages are cached on a per-engine basis. This means that if a translation service adds additional translatable languages, this cache must be cleared on the module config page, via the Fluency module API, or via an AJAX request using the Fluency admin REST API.
Fluency provides tools to help make building websites with multi-language capabilities faster and easier. There are HTML best practices that help users with accessibility and improve SEO performance, many of these are available out of the box with Fluency.
Language names and ARIA attributes are automatically localized when you configure Fluency and use the ProcessWire core translation pages to translate the module.
Returns the target language ISO code provided by the third party service.
// 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'
Tip: Use this to indicate the current language of the page including the lang
attribute on the <html>
tag.
<!doctype html>
<html lang="<?= $fluency->getLanguageCode(); ?>">
<!-- markup -->
</html>
Fluency can render a list of languages <link>
tags that you can use in the <head>
of your HTML document. This helps users and search engines find the content for your page in all languages. The URLs will render as configured in ProcessWire.
To render the tags:
<!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>
Output:
<!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>
You can easily render a <select>
element that will allow a user to choose the langauge that they are currently viewing the page in. By default, Fluency will also render inline JavaScript that will navigate to the page in the language selected but this can be disabled should you want to control that behavior yourself. All text/labels/values will render in the current language if translated and as configured in ProcessWire.
<div class="language-select"><?= $fluency->renderLanguageSelect() ?></div>
Output:
<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>
With options:
<div class="language-select">
<?= $fluency->renderLanguageSelect( addInlineJs: false, id: 'my-custom-id', classes: ['some',
'classes'] ) ?>
</div>
Output:
<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>
You can also render an unordered list of links to the current page in other languages.
<div class="languages"><?= $fluency->renderLanguageLinks(); ?></div>
Output:
<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>
With options:
<div class="languages">
<?= $fluency->renderLanguageLinks( id: 'my-language-links', classes: ['my-class'], divider: '|',
activeClass: 'current'); ?>
</div>
Output:
<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 can be accessed anywhere in ProcessWire using the $fluency
variable. The following are some simple examples. For more in-depth details, review the docblocks for each method in Fluency.module.php
.
Fluency is fully documented and formatted for the API Explorer tool in the outstanding (and personally recommended) ProDevTools module.
All methods return Data Transfer Objects that are immutable and predictable in structure and features. All values can be accessed via:
- Object properties
$dtoObject->property
- Be converted to an array using the
$dtoObject->toArray()
method - Encoded to JSON directly using
json_encode($dtoObject)
Data Transfer Objects also contain helper methods to access or find data within the 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
Translate multiple strings simultaneously
$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
This method returns all languages that the current translation service recognizes. This includes languages that aren't configured in Fluency.
The source/target language codes can be used when calling $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 provides robust data for languages configured in Fluency. In certain instances it may be preferrable to using ProcessWire's $language
object when modifying state is not required.
$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
Note: not all translation services provide this information
$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
AJAX calls can be made to endpoints in the ProcessWire admin to interact with Fluency. The current user must have the fluency-translate
permission. Refer to Fluency.module.php
method docblocks to review accepted methods and responses. Base admin URL will match your ProcessWire installation.
$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 handles errors encountered when communicating with the translation API and errors experienced in ProcessWire. All Data Transfer Objects contain error
and message
properties.
error
- Will contain an error type string defined as class constants in in app/FluencyErrors.php
message
- Will contain a human-friendly message for the error type that occurred. These are located in app/FluencyLocalization.php
and can be translated
Translation Engine and third party service errors are stored under the fluency-engine
log. If there are issues with translating content, refer to that log first to check whether the translation service is experiencing issues.
- The browser plugin for Grammarly may conflict with Fluency and is a known issue for many web apps. If you encounter issues, the solution is to either disable Grammarly while using Fluency in the ProcessWire admin, or log into the admin in a private browser window where Grammarly may not be running. Instructions provided by Grammarly here.
Module Features
Feature suggestions are welcome. In fact, Fluency has been made better thanks to great feedback by the ProcessWire community. If you have a suggestion, file an issue in the Fluency GitHub repository.
Translation Engines
Fluency is modular in that it contains a framework for adding additional third party services as "Translation Engines". You can choose which Translation Engine you prefer and provide the credentials to connect via their API. As this project is open source, contributions for new third party services as Translation Engines are welcome. If you would like to request a new third party service, file an issue in the Fluency GitHub repository
If you'd like to develop one for yourself, feel free to fork Fluency, build a new Translation Engine, and create a pull request.
Developer documentation for integrating third party translation services as Translation Engines is located in Fluency/app/Engines/DEVDOC.md
. Documentation is a work in progress. If you need help or more information, feel free to send me a PM on the ProcessWire forum.
Fluency is free to use. There is no cost for this module and it can be used on any site that you build with ProcessWire. This module is provided as a thank you to the outstanding ProcessWire community and as a contribution alongside other module developers who help everyone build great websites and applications.
Module development is both a lot of work and an important contribution to the ProcessWire ecosystem. Developers who offer both free and paid modules spend many hours creating and maintaining our favorite tools we all use for ProcessWire projects.
You can support module development by giving the Github repo a star, donating, or contributing to development with pull requests. If you find yourself reaching for a module that you can't live without, or if a module fulfills a need for a client request, consider donating or sponsoring a developer or their projects.
While this message is for the ProcessWire community on behalf of all module developers, if you've found Fluency useful and think it's worth a cup of coffee, you can send me something via PayPal.