Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add internationalization support #738

Merged
merged 5 commits into from
Jun 11, 2023
Merged

Conversation

brawaru
Copy link
Contributor

@brawaru brawaru commented Nov 16, 2022

This PR adds basic internationalisation support using the VIntl module, developed by me, specifically for Modrinth, based on the implementation present in the previous iteration of this pull request.

For maintainers #

This PR adds @vintl/nuxt module, which adds integration for VIntl, a Vue plugin that integrates FormatJS Intl with additional features, such as locale loading, changing, events, formatted components, etc. I encourage you to read the linked documentations for more information on how to use the APIs at your disposal.

There are also additional extensions for FormatJS Intl, such as compact-number (a hacky way to pluralise numbers in compact notation more properly) and how-ago (based on implementation of $ago by @venashial in Omorphia).

For translators #

As a translator myself I tried to make experience for us translators as good as I possible can. That included adopting FormatJS Intl instead of using something like nuxt-i18n with vue-i18n. That means we are getting the ICU MessageFormat that is widely used, instead of custom interpolation provided by aforementioned modules/plugins.

FormatJS have a good summary of ICU MessageFormat syntax available on their site. If you are not familiar with this syntax, I encourage you to read the summary. It's pretty simple syntax that gets the job done. At our hands are {variables} (aka arguments), {plurals, plural, one {# plural} other {# plurals}}} (where # will be replaced with the number, but see the note below), {selectors, select, are_cool {are cool} other {are so-so}}, dates and other formats.

Note

When using plurals, in cases of numbers in compact notation (e.g. 1.2K), the number used as plural argument is not a real number: it's rounded to match the formatted number. To display the actual number you must use the same argument you used to calculate the plural, but as a value. Do not use # shorthand in such cases. Follow the original strings format!

Example: you may see {versions, plural, one {# version} other {# versions}}, in this case you can assume we won't display compact number, therefore you can use # shorthand. However you may also come across {followers, plural, one {{followers} follower} other {{followers} followers}}, in this case we are probably displaying followers in compact notation on frontend, so please do not use #, so you won't get 1,200 followers when in reality there are 1,246 of them (or 1.2K, as we wanted to display it, and if you used {followers}, not #).

There are also additional features like rich text formatting. This lets you format entire components in a safe and easy-to-understand way. It's kind of like HTML, but not really.

To use this feature, you just need to know that each formatted component has a start tag (which looks like <my-tag>) and an end tag (which looks like </my-tag>). Everything between these tags will be formatted. You can also use variables and other tags inside the tags, as long as they're in the source string.

You can disable specific tags by putting them in quotation marks, like this: Use the '<details>' tag to hide optional content.

Crowdin should help you to reduce the noise by mangling the tag names into zero-indexed numbers. E.g. <strong>Warning:</strong> you are now warned. <italic>Please be mindful of that</italic>. will become <0>Warning:</0> you are now warned. <1>Please be mindful of that</1>..

Note

You still can see how Crowdin will interpret short tags by hovering over them with your mouse. If you click on that tag, it will be added to translation input field.

As we progress with localisation, I will try to add screenshots and even edit context for some strings that use variables, but otherwise variable names should represent well what values they hold, and translation keys tell where the translation is used, but don't fear to ask in Crowdin comments for additional context.

Merge and integration #

Step 1. Merge this pull request

This pull request currently does not have any major user facing changes, so it's safe to merge as is.

Step 2. Connect Crowdin

After the merge of this pull request, you should connect this repository to Crowdin through Integrations / GitHub settings (only project managers can see this page). To do so, open the linked page and:

  1. Click ‘Plus icon Add Repository’.
  2. In dropdown menu choose ‘Source and translation files mode’.
  3. In open modal select Modrinth organisation for account and ‘knossos’ repository.
  4. Select ‘master’ branch for translation. Optionally rename branch name for translations.
  5. Toggle ‘Always import new translations from the repository’ on.
  6. Toggle ‘Push Sources’ on.
  7. Click ‘Save’.

If you're feeling adventurous, you can learn about Crowdin CLI and set up Crowdin GitHub Action instead, which will give you more control on how the translations are synced.

Explanations:

  • By turning on ‘Always import new translations from the repository’ translations can be provided through this repository, however it is recommended that everyone is forced to contribute translations through Crowdin — since submitting translations involves collaboration and verification of new translations by other translators and proofreaders.
  • Push sources will not push anything to master. This just allows project managers on Crowdin change source strings (where file format allows) and Crowdin will update the source file in the next translations pull request.

After connection, Crowdin will immediately start synchronisation. Our Crowdin bot on GitHub will create a branch for master, and pull request that adds translations, it will be constantly updated at configured interval and eventually needs to be merged. Delete bot's branch to force a rebase (deleting a branch closes active pull request).

Step 3. Adjust workflow.

After this feature is finally merged, some adjustments have to be made to workflow.

  • Translating features of mass interest. When you know that feature you are currently working on will be announced to public and might gain public interest, consider giving translators a chance to translate it during the beta testing by manually enabling feature branches on Crowdin or leaving feature on staging for some time and then syncing translations before merging to prod.

    By doing this you are ensuring that users in other languages will have better experience and first impression from the new feature since it's already translated.

    For example, monetisation was a feature of mass interest since it was anticipated and announced everywhere, so it'd made sense to translate it before release. On the other hand, Re-word "my profile" to "your profile"/"this profile" #766, which changed wording in menu from ‘my profile’ to ‘your profile’ could've been merged as is, with translations coming later to master.

  • (Not necessarily) Frequent master translation merges after new features are out. In addition to above, when any new features are released, it makes sense to merge translations more often (e.g. once in two days vs once a week), since untranslated feature might catch attention of speakers of languages where it was not translated and they might go to Crowdin and contribute the translations.

  • Adding languages. As mentioned above, to add languages to the site you have to explicitly enable them in nuxt.config.js, otherwise translations will exist but won't be used. Remember to also configure Crowdin language mappings to use correct locale codes (widened BCP 47 tags). They are already configured for existing languages, but need to be configured for new ones.

    Also remember that some ISO language codes (required for SEO) don't match BCP 47 tags, for those locales meta.static.iso needs to be added to locale descriptor under vintl.locales[?] of nuxt.config.js.

  • Extract translations. Never edit locales/en-US/index.json manually. Instead, run pnpm intl:extract, which will iterate over all of the files in the project and extract the descriptors defined using defineMessage or defineMessages meta-functions.

Additional information #

If you remember, this pull request previously included translations for all of the pages, as well as a language picker. These were specifically omitted to take a different approach on things: instead of shipping everything at once, pages will be made translatable one by one, and features like language switcher will be added in separate pull requests.

The reason for this approach is that housekeeping a huge amount of changes (which make pages translatable) is a lot of work, and very difficult. Doing pages one by one and keeping PRs like this small makes them much easier to review.

Rest assured, the work already done is not lost, every revision lives in its own branch of my fork, and can be used as a reference:

Other things to note:

  • Unfortunately, we are not going to support Right-to-Left languages at first, as we cannot guarantee proper support for these due to maintenance costs that we are not willing to take. We may enable RTL languages on staging version of the site for testing and allow contribution of fixes for them.

  • This will not change anything regarding the content on the site, which will still be provided mostly in English language. There's an open idea about adding translatable project descriptions, go upvote it! As for now, you can use details blocks as a workaround.

But I see you edit frog page!! Why?! How dare you!

I know the frog page is is sacred and inviolable, but it's the least updated and visible page of the site, so it makes it a good initial playground to see that everything is working correctly. I hope the frog society can forgive me for this, as it's only temporary, and done in a very frog way.

Credits #

Some inspiration was taken from nuxt-i18n module and vue-i18n plugin APIs. Their libraries provided guidance of how changes can be implemented in the early days. Also, big thanks to FormatJS authors and contributors for providing libraries that power this contribution.

Support my work. This contribution took a lot of time and energy, so if you appreciate my efforts, please consider supporting me, this would really help out these days.

@brawaru
Copy link
Contributor Author

brawaru commented Dec 3, 2022

Finished documenting the pull request and created supplementary pull request for placeholder locale and Crowdin config, this is now ready for review!

Copy link
Contributor Author

@brawaru brawaru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a couple of things for the beginning.

plugins/i18n-helpers.js Outdated Show resolved Hide resolved
.prettierrc Outdated Show resolved Hide resolved
crowdin.yml Outdated Show resolved Hide resolved
nuxt.config.js Outdated Show resolved Hide resolved
vercel.json Outdated Show resolved Hide resolved
modules/i18n/templates/vueUtils.js Outdated Show resolved Hide resolved
modules/i18n/templates/vueUtils.js Outdated Show resolved Hide resolved
modules/i18n/consts.js Outdated Show resolved Hide resolved
modules/i18n/forceNumberFormatPolyfill.js Outdated Show resolved Hide resolved
modules/i18n/forceNumberFormatPolyfill.js Outdated Show resolved Hide resolved
@brawaru
Copy link
Contributor Author

brawaru commented Jan 12, 2023

Rebased. All translations have been removed, language switching modal is not included (will be added via a separate PR).

Previous state lives here.

@brawaru
Copy link
Contributor Author

brawaru commented May 12, 2023

I think the cache is just busted, causing CI to fail. Installing works on my end and GitHub Codespaces.

Update as per 27 May, 2023: Nah, that's just Yarn being annoying. Not sure how to solve this issue, will try something later.

@triphora triphora linked an issue May 14, 2023 that may be closed by this pull request
@triphora triphora removed a link to an issue May 14, 2023
@brawaru
Copy link
Contributor Author

brawaru commented May 29, 2023

Rebased to use pnpm (Yarn workaround removed), fixed the Crowdin config, and updated the description of pull request! 🙌

@brawaru
Copy link
Contributor Author

brawaru commented Jun 10, 2023

Rebased, all the VIntl dependencies are updated and pin specific FormatJS packages versions due to formatjs/formatjs#4126. #CJSMustDie

@triphora triphora merged commit de99104 into modrinth:master Jun 11, 2023
1 of 2 checks passed
Morb0 pushed a commit to CorvaxStation/knossos that referenced this pull request Jun 30, 2023
brawaru added a commit to brawaru/omorphia that referenced this pull request Oct 24, 2023
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
brawaru added a commit to brawaru/omorphia that referenced this pull request Oct 24, 2023
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
brawaru added a commit to brawaru/omorphia that referenced this pull request Oct 24, 2023
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
brawaru added a commit to brawaru/omorphia that referenced this pull request Oct 24, 2023
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
brawaru added a commit to brawaru/omorphia that referenced this pull request Oct 27, 2023
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants