Skip to content

Instantly share code, notes, and snippets.

@Lemmings19
Last active July 30, 2021 20:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lemmings19/685770caa52d9f3faf02b1bb834e9b17 to your computer and use it in GitHub Desktop.
Save Lemmings19/685770caa52d9f3faf02b1bb834e9b17 to your computer and use it in GitHub Desktop.
React i18next Translation Tips
TRANSLATION TIPS
2021-07-26
// PREFACE:
// These are custom to the project I am working on. For you, these two references:
withTranslationLoaded
WithTranslationLoadedProps
// Should probably be:
withTranslation
WithTranslationProps
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
// STEP 1: Import the translation functions.
// We need to import the translation functions. We do this differently based on
// how the file we are working with is written.
// SCENARIO A
// Use this in classes or functions where a prop parameter is being taken in.
// This will flash a loading spinner if the translations haven't loaded for that
// component yet. It will also import the necessary functions for adding
// translations:
import withTranslationLoaded, {
WithTranslationLoadedProps,
} from 'lib/client/i18n/withTranslationLoaded';
// Then, find where the class is called, and add this to the props:
WithTranslationLoadedProps
// eg.
class SomeClass extends React.Component<SomeClassProps> {}
// becomes:
class SomeClass extends React.Component<SomeClassProps & WithTranslationLoadedProps> {}
// Then, find the export and wrap it in the Higher Order Component:
export default SomeClass;
// becomes:
export default withTranslationLoaded('someNamespace')(SomeClass);
// SCENARIO B
// Use a hook. (does not work in classes)
import { useTranslation } from 'react-i18next';
// Then, instantiate it:
const { t } = useTranslation(['someNamespace']);
// SCENARIO C
// When dealing with some .jsx files, these patterns may change:
class MyClass extends React.Component {
static propTypes = {
foo: PropTypes.string
}
}
export default MyClass;
// becomes:
import withTranslationLoaded from 'lib/client/i18n/withTranslationLoaded';
. . .
class MyClass extends React.Component {
static propTypes = {
foo: PropTypes.string,
t: PropTypes.func.isRequired
}
}
export default withTranslationLoaded(['myNamespace'])(MyClass);
// SCENARIO D
// There are some use cases where these first two don't work. If you run into one
// of these, just reach out for help or see if you can figure it out; whichever is faster.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
// STEP 2: Select a *namespace* and *key* for the file you are working on.
// In the previous examples, `someNamespace` tells the translator which batch of
// translations to load in. We usually name this relative to the last folder or two
// in the directory we are working in. For example:
src/ui/client/components/Landing/home.tsx
// would have a namespace of:
componentsLanding
// It is your job to come up with a sensible namespace, or use the one that is
// already in use. I would probably still use `componentsLanding`, even if the
// full path is:
src/ui/client/components/Landing/pages/home/home.tsx
// Unless ./pages or ./pages/home has a whole lot of files and translations and
// is probably worthy of its own namespace.
// Next, we make the key. The key starts with the filename that we are working on,
// followed by a period, and then the string we are working on. For example, when
// working in the aforementioned `devices.jsx` file, our key would start with:
devices.
// And if we are wanting to translate the string "The quick brown fox", our full
// key would be:
devices.theQuickBrownFox
// or
devices.quickBrownFox
// or, the key might be more contextual. For instance, if this is for a title on
// a specific form named "Animal Description", you might choose the key:
devices.animalDescriptionTitle
// Choose what you think makes the most sense, and will give the translator
// reading it the most meaningful and relevant context.
// Aside from being readable, it is imperative that our namespaces and keys form
// a UNIQUE combination. We want to avoid any potential for overlap across
// files.
// Our namespace and key is broken down like this:
// namespace:filename.stringKey
// `namespace` provides a convenient grouping for translations. Each namespace will
// get its own file full of translations.
// `filename` should match the current file name, unless it is generic and there is
// a real risk of overlap with another file in the same namespace.
// `stringKey` should convey the meaning or purpose of the individual string being
// translated.
// A script is run that will scour the codebase for translations and generate a JSON
// object that contains all of the translations. It will generate a file based on
// the namespaces and keys that you choose:
{
"namespace": {
"filename1": {
"all": "All",
"logout": "log out",
"sortBy": "SORT BY"
},
"filename2": {
"all": "ALL",
"logout": "Log Out",
"durationRange": "Last {{count}} days",
"durationRange_plural": "Last {{count}} days",
"sortBy": "Sort By"
"adminNotice": "This is a notice! <0>This is something within a nested component.</0>"
}
}
}
// To keep our translations from conflicting with one another, and to keep the
// namespaces and keys predictable, we should follow this standard:
// NAMESPACE should ONLY be aware of the path that it is in. Generally, it should
// NOT reference any individual filename or string name contained within.
// FILENAME should ONLY be aware of the filename that it exists within. It should
// NOT reference the path or the string(s) included in that file.
// STRINGKEY should ONLY be aware of the individual string being translated. It
// should NOT reference the filename or path.
// Combining the `namespace`, `filename`, and `stringKey` should provide a wholly
// unique combination. Keeping this structure helps to avoid *most* potential
// conflicts between files. An example of a poor implementation would look like:
{
"namespace": {
"sharedName": {
"allFilename1": "All",
"allFilename2": "ALL",
"logout": "log out",
"logoutFilename2": "Log Out",
"durationRange": "Last {{count}} days",
"durationRange_plural": "Last {{count}} days",
"sortByFilename1": "SORT BY",
"sortByFilename2": "Sort By",
"adminNotice": "This is a notice! <0>This is something within a nested component.</0>"
}
}
}
// In the event that there are two very similar strings in the same file, write the
// `stringKey` differently between the two. For example:
<>Quick brown fox</>
<>+ Quick brown fox</>
// becomes:
<>{t('namespace:filename.quickBrownFox', 'Quick brown fox')}</>
<>{t('namespace:filename.plusQuickBrownFox', '+ Quick brown fox')}</>
// We then put the key and namespace together:
componentsLanding:devices.quickBrownFox
// Note that translation function imports only use the namespace, while string
// translations use the full namespace and key combination.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
// STEP 3: Translate!
// Translations can be handled in a few different ways based on the context.
// The first thing we need to do is instantiate our translation function.
// If we used SCENARIO A for our import, just access the props of the class
// you are working in:
const { t } = this.props;
// Or if it's a function and not a class, the pattern is usually:
const { t } = props;
// If you used SCENARIO B:
const { t } = useTranslation(['someNamespace']);
// Then, translate your string!
The quick brown fox
// becomes:
{t('someNamespace:currentFilename.quickBrownFox', 'The quick brown fox')}
// Done!
// But if you have to handle something with plurals...
var foxCount = 2;
. . .
The quick brown fox{foxCount != 1 ? 'es' : ''}
// becomes:
{
t('someNamespace:currentFilename.quickBrownFox',
{
defaultValue: 'The quick brown fox',
defaultValue_plural: 'The quick brown foxes',
count: foxCount,
}
}
// or if you need to show the count...
{
t('someNamespace:currentFilename.quickBrownFox',
{
defaultValue: 'There is {{count}} quick brown fox',
defaultValue_plural: 'There are {{count}} quick brown foxes',
count: foxCount,
}
}
// If you have more variables in the string, you can do:
var foxColor = red;
. . .
{
t('someNamespace:currentFilename.quickBrownFox',
{
defaultValue: 'There is {{count}} quick {{foxColor}} fox',
defaultValue_plural: 'There are {{count}} quick {{foxColor}} foxes',
count: foxCount,
foxColor: foxColor,
}
}
// or...
var foxColor = red;
. . .
{
t('someNamespace:currentFilename.quickBrownFox',
{
defaultValue: 'There is a quick {{foxColor}} fox',
foxColor: foxColor,
}
}
// Note that `defaultValue`, `defaultValue_plural`, and `count` are KEYWORDS,
// and anything else is custom.
// And what if there are components or HTML tags INSIDE the string you need to
// translate? Well, we need to import a different component for that. `t()` will
// not work. Use the Trans component!
The quick <FoxColor>brown</FoxColor> fox
// becomes:
import { Trans } from 'react-i18next';
. . .
<Trans i18nKey="someNamespace:currentFilename.quickBrownFox">
The quick <FoxColor>brown</FoxColor> fox
</Trans>
// And if you need variables:
var animal = {name: 'dog'};
. . .
<Trans i18nKey="someNamespace:currentFilename.quickBrownAnimal">
The quick <FoxColor>brown</FoxColor> {{animalName: animal.name}}
</Trans>
// Sometimes, we will need to translate something outside of a function or class
// that we are working on.
// If you need to translate a string from the database, just ignore it. We do not
// currently (2021-07-26) have a solution for that.
// If it's a set of constants, we will need to refactor those constants into a
// new function.
cost MY_CONSTANTS = ['foo', 'bar'];
// becomes:
import { TFunction } from 'react-i18next';
. . .
function getMyConstants (t: TFunction) {
return [
t('someNamespace:currentFilename.foo', 'foo'),
t('someNamespace:currentFilename.bar', 'bar'),
];
}
// For obvious reasons, you will need to replace all existing calls to MY_CONSTANTS
// to getMyConstants(). Watch out for exported constants that will need additional
// refactoring in other files!
// Note that we are passing in the `t()` function from whoever calls these constants.
// If you have a better solution, share it with the team! This is just the best that
// I came up with.
// If we find a variable inside of a string which may be subject to change, such as
// a number, phone number or address, we should extract it like so:
Upgrade to see beyond 14 days
// becomes:
t('namespace:filename.upgradeToSeeFurther', {
defaultValue: 'Upgrade to see beyond {{dayCount}} days',
dayCount: 14,
})
// This allows us to change this value in the future without needing to re-translate
// this string in all languages.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
// GOTCHAS AND TIPS!
// There may be other edge cases that are undocumented here. If you find yourself
// stuck, do not hesitate to reach out to the dev team!
// WithTranslationLoaded is something custom that we wrote to wrap around the
// stock TranslationLoaded. Ours might not work in every scenario!
// Note that the `t()` function does not know how to handle strings that are
// concatenated. The string that you use should be one long string, even if it
// breaks our styleguide's line length limits.
// The `t()` function also requires that you use single or double quotes. You
// cannot wrap strings in backticks.
// It may be handy to keep a cheatsheet of imports and functions that you'll want
// to re-use between files. I recommend it! However, be careful when
// copying and pasting. It's very easy to put the wrong namespace or key into
// the wrong file when copy+pasting.
// If you are translating a lot of files at once, I recommend against writing the
// full namespace and key into your cheatsheet. I only write in the namespace in
// my cheatsheet, and I do my best to remember to change it every time I move folders.
// You can rely on the linter and our tests for a lot when translating, but they
// will not catch mistakes in namespaces or keys, or a few other things:
// If you use the hook in the wrong place (`import { useTranslation } from 'react-i18next';`)
// the linter won't complain, but the page won't actually work. This will only be
// caught in testing or UI tests. It's easy for this to slip into production if
// a component isn't explicitly tested. But it's easy to identify. The error it
// produces will look like: "Invalid hook call"
// When running `lint i18next:upload` (see Translation Management.md in our project's
// docs), the error:
ERROR projectName : Found translation key already mapped to a map or parent of new key
already mapped to a string: filename.stringName
// Means that there existts a translation without a stringName. Search for ":filename.'"
// and you should find an incomplete translation key somewhere. Fix it to fix the error.
// For translation keys and namespaces, it's good to review the output produced
// by `yarn i18next:upload` to look for mistakes or places where you missed a key.
// But for most incorrect namespaces, you'll only catch that with good code review.
// This is what my cheatsheet looks like: (I keep it open on the side window of my IDE)
import withTranslationLoaded, {
WithTranslationLoadedProps,
} from 'lib/client/i18n/withTranslationLoaded';
withTranslationLoaded(['teacherTracker'])
//
import { TFunction } from 'react-i18next';
//
import { useTranslation } from 'react-i18next';
// Choose whichever fits the context you are working on:
const { t } = props;
const { t } = this.props;
const { t } = useTranslation(['namespaceIAmWorkingOn']);
// Easy copy+paste for translations:
t('namespaceIAmWorkingOn:fileIAmWorkingOn.', '')
{t('namespaceIAmWorkingOn:fileIAmWorkingOn.', '')}
{t('namespaceIAmWorkingOn:fileIAmWorkingOn.', { defaultValue: '', other: '' })}
// Not generally used:
import {
withTranslation,
WithTranslation as WithTranslationProps,
} from 'react-i18next';
WithTranslationProps
withTranslation(['namespaceIAmWorkingOn'])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment