Last active
July 30, 2021 20:37
-
-
Save Lemmings19/685770caa52d9f3faf02b1bb834e9b17 to your computer and use it in GitHub Desktop.
React i18next Translation Tips
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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