When developing custom modules for Drupal, translating the user interface strings can not benefit from the localize.drupal.org infrastructure, like for contrib modules. So you need a way to provide these UI translations in your custom module (or a custom language server) individually.
As this is poorly documented (or I didn't find it at least), I've decided to document this in my blog. Best information regarding this can perhaps be found here: https://www.drupal.org/community/contributor-guide/reference-information/localize-drupal-org/working-with-offline/po-and-pot-files
In Drupal 7 I remember there was a folder-based default process to import translations from the module folder, if a translations folder exists with LANGUAGE.po files in it. Then these were automatically imported. But that's not the case any more for Drupal 8+.
Extract strings to translate (.pot)
To extract the strings to translate from the module code, use https://www.drupal.org/project/potx
This should not be added as dependency of the module and only needs to be used for the first translation or when strings are added or changed.
Translate the .pot files into the deserved languages and make them available
To translate the .pot files and create .po files, there are several tools. One widely known is https://poedit.net/
Let the module know about your translations
Then we have to add information to the .info.yml file to inform Drupal that the translations in the "translations" folder.
The important information can be found in the locale.api.php file (I'd call this hidden important information):
/**
* @defgroup interface_translation_properties Interface translation properties
* @{
* .info.yml file properties for interface translation settings.
*
* For modules hosted on drupal.org, a project definition is automatically added
* to the .info.yml file. Only modules with this project definition are
* discovered by the update module and use it to check for new releases. Locale
* module uses the same data to build a list of modules to check for new
* translations. Therefore modules not hosted at drupal.org, such as custom
* modules, custom themes, features and distributions, need a way to identify
* themselves to the Locale module if they have translations that require to be
* updated.
*
* Custom modules which contain new strings should provide po file(s) containing
* source strings and string translations in gettext format. The translation
* file can be located both local and remote. Use the following .info.yml file
* properties to inform Locale module to load and import the translations.
*
* Example .info.yml file properties for a custom module with a po file located
* in the module's folder.
* @code
* 'interface translation project': example_module
* 'interface translation server pattern': modules/custom/example_module/%project-%version.%language.po
* @endcode
*
* Streamwrappers can be used in the server pattern definition. The interface
* translations directory (Configuration > Media > File system) can be addressed
* using the "translations://" streamwrapper. But also other streamwrappers can
* be used.
* @code
* 'interface translation server pattern': translations://%project-%version.%language.po
* @endcode
* @code
* 'interface translation server pattern': public://translations/%project-%version.%language.po
* @endcode
*
* Multiple custom modules or themes sharing the same po file should have
* matching definitions. Such as modules and sub-modules or multiple modules in
* the same project/code tree. Both "interface translation project" and
* "interface translation server pattern" definitions of these modules should
* match.
*
* Example .info.yml file properties for a custom module with a po file located
* on a remote translation server.
* @code
* 'interface translation project': example_module
* 'interface translation server pattern': http://example.com/files/translations/%core/%project/%project-%version.%language.po
* @endcode
*
* Custom themes, features and distributions can implement these .info.yml file
* properties in their .info.yml file too.
*
* To change the interface translation settings of modules and themes hosted at
* drupal.org use hook_locale_translation_projects_alter(). Possible changes
* include changing the po file location (server pattern) or removing the
* project from the translation update list.
*
* Available .info.yml file properties:
* - "interface translation project": project name. Required.
* Name of the project a (sub-)module belongs to. Multiple modules sharing
* the same project name will be listed as one the translation status list.
* - "interface translation server pattern": URL of the .po translation files
* used to download the files from. The URL contains tokens which will be
* replaced by appropriate values. The file can be locate both at a local
* relative path, a local absolute path and a remote server location.
*
* The following tokens are available for the server pattern:
* - "%core": Core version. Value example: "8.x".
* - "%project": Project name. Value examples: "drupal", "media_gallery".
* - "%version": Project version release. Value examples: "8.1", "8.x-1.0".
* - "%language": Language code. Value examples: "fr", "pt-pt".
*
* @see i18n
* @}
*/
(https://api.drupal.org/api/drupal/core!modules!locale!locale.api.php/11…)
Based on your release process, you may use a simple pattern like
/translations/%language.po
which is similar to the one used in Drupal 7 before, or the more complex one from the example:
/translations/%project-%version.%language.po
Ensure to name the .po files accordingly and to have the locale module enabled, when testing the translation.
If the module is already enabled, you may need to "Update translations" from the status report or use the following drush commands:
locale:check
locale:update