7.5 KiB
Internationalization and Localization
General description
Localization support (translations) in Grist is implemented via https://www.i18next.com javascript library. It is used both on the server (node) and the client side (browser). It has very good documentation, supports all needed features (like interpolation, pluralization and context), and has a rich plugin ecosystem. It is also very popular and widely used.
Localization setup
Resource files are located in a static/locales
directory, but Grist can be configured to read them
from any other location by using the GRIST_LOCALES_DIR
environmental variable. All resource files
are read when the server starts. The default and required language code is en
(English), all other
languages are optional and will be supported if server can find a resource file with proper language
code. Languages are resolved hierarchically, from most specific to a general one, for example, for
Polish code pl-PL, the library will first try pl-PL, then pl, and then will fallback to a
default language en (https://www.i18next.com/principles/translation-resolution).
All language variants (e.g., fr-FR, pl-PL, en-UK) are supported if Grist can find a main
language resource file. For example, to support a fr-FR language code, Grist expects to have at
least fr.client.json file. The main language file will be used as a default fallback for all French
language codes like fr-FR or fr-CA, in case there is no resource file for a specif variant (like
fr-CA.client.json
) or some keys are missing from the variant file.
Here is an example of a language resource file en.client.json
currently used by Grist:
{
"AddNewButton": {
"AddNew": "Add New"
},
"DocMenu": {
"OtherSites": "Other Sites",
"OtherSitesWelcome": "You are on the {{siteName}} site. You also have access to the following sites:",
"OtherSitesWelcome_personal": "You are on your personal site. You also have access to the following sites:",
"AllDocuments": "All Documents",
"ExamplesAndTemplates": "Examples and Templates",
"MoreExamplesAndTemplates": "More Examples and Templates"
},
"HomeIntro": {
"Welcome": "Welcome to Grist!",
"SignUp": "Sign up"
}
}
It maps a key to a translated message. It also has an example of interpolation and context features
in the DocMenu.OtherSitesWelcome
resource key. More information about how to use those features can be
found at https://www.i18next.com/translation-function/interpolation and
https://www.i18next.com/translation-function/context.
Client and server code (node.js) use separate resource files. A resource file name format
follows a pattern: [language code].[product].json (e.g. pl-Pl.client.json
, en-US.client.json
,
en.client.json
). Grist can be packaged as several different products, and each product can have its
own translation files that are added to the core. Products are supported by leveraging i18next
feature called namespaces
https://www.i18next.com/principles/namespaces.
For now we use only two products called client
and server
.
Each of them is then organized by filename, in order to avoid conflicts.
Translation instruction
Client
The entry point for all translations is a function exported from 'app/client/lib/localization'.
import {t} from 'app/client/lib/localization';
It is a wrapper around i18next
exported method with the same interface
https://www.i18next.com/overview/api#t. As a future improvement, all resource keys used in
translation files will be extracted and converted to a TypeScript definition file, for a “compile”
time error detection and and better development experience. Here are couple examples how this method
is used:
app/client/ui.DocMenu.ts
css.otherSitesHeader(
t('DocMenu.OtherSites'),
.....
),
dom.maybe((use) => !use(hideOtherSitesObs), () => {
const personal = Boolean(home.app.currentOrg?.owner);
const siteName = home.app.currentOrgName;
return [
dom('div',
t('DocMenu.OtherSitesWelcome', { siteName, context: personal ? 'personal' : '' }),
testId('other-sites-message')
app/client/ui/HomeIntro.ts
function makeAnonIntro(homeModel: HomeModel) {
const signUp = cssLink({href: getLoginOrSignupUrl()}, t('HomeIntro.SignUp'));
return [
css.docListHeader(t('HomeIntro.Welcome'), testId('welcome-title')),
Function t
on the client side is also able to use DomContents
values (so material produced by
the GrainJS library) for interpolation. For example:
dom('span', t('Argument', {
arg1: dom('span', 'First'),
arg2: dom.domComputed(obs, (value) => dom('span', value))
}));
Some things are not supported at this moment and will need to be addressed in future development tasks:
- Date time picker component. It has its own resource files that are already imported by Grist but not used in the main application. https://bootstrap-datepicker.readthedocs.io/en/latest/i18n.html
- Static HTML files used as a placeholder (for example, for Custom widgets).
- Formatting dates. Grist is using
moment.js
library, which has its own i18n support. Date formats used by Grist are shared between client, server and sandbox code and are not compatible withi18next
library.
Server
For server-side code, Grist is using https://github.com/i18next/i18next-http-middleware plugin,
which exposes i18next
API in the Request
object. It automatically detects user language (from
request headers) and configures all API methods to use the proper language (either requested by the
client or a default one). Comm
object and webSocket
API use a very similar approach, each
Client
object has its own instance of i18next
library configured with a proper language (also
detected from the HTTP headers).
Naturally, most of the text that should be translated on the server side is used by the Error handlers. This requires a significant amount of work to change how errors are reported to the client, and it is still in a design state.
Here is an example of how to use the API to translate a message from an HTTP endpoint in
HomeServer
.
app/server/lib/sendAppPage.ts
function getPageTitle(req: express.Request, config: GristLoadConfig): string {
const maybeDoc = getDocFromConfig(config);
if (!maybeDoc) {
return req.t('sendAppPage.Loading') + '...';
}
return handlebars.Utils.escapeExpression(maybeDoc.name);
}
Next steps
- Annotate all client code and create all resource files in
en.client.json
anden.server.json
files. Almost all static text is ready for translation. - Store language settings with the user profile and allow a user to change it on the Account Page. Consider also adding a cookie-based solution that custom widgets can use, or extend the WidgetFrame component so that it can pass current user language to the hosted widget page.
- Generate type declaration files at build time to provide
missing key
error detection as soon as possible. - Dynamically Include calendar control language resource files based on the currently selected language.
- Refactor server-side code that is handling errors or creating user-facing messages. Currently, error messages are created at the place where the Error has occurred. Preferably errors should include error codes and all information needed to assemble the error message by the client code.
- Add localization support to the
moment.js
library to format dates properly according to the currently selected language. - Add support for custom HTML page translation. For example
custom-widget.html