@chayns/textstrings
v1.0.1
Published
This package is supposed to replace the textstring component from chayns-components (both v4 and v5).
Readme
@chayns/textstrings
This package is supposed to replace the textstring component from chayns-components (both v4 and v5).
This package also includes CLI tools to update local textstring files, generate textstring files and create/update textstrings. And of couse the textstring component.
Even though the suggested changes to the component could still be integrated into the v5, the CLI tools should not be a part of the chayns-components (controversial opinion: the component should not be either).
TOC
Config
Create a textstrings.config.js in your project root or call npx textstrings init (or npx txt init)
module.exports = {
libraries: [
{
prefix: 'txt_chayns_',
libraryName: '<YourLibraryName>',
// alias: 'shared', optional use different alias and filename for named exports. only relevant when singleFile is false or you have multiple libraries
// filePath: undefined, optional configurable paths for each library
},
// ...
],
defaultLanguage: 'Ger', // default: 'Ger', not all languages supported yet
folderPath: './src/constants/textStrings', // folder where the constant files should be created, optional
singleFile: false, // default: true, only available when using exactly one library, optional
format: 'json', // 'json' | 'js' | 'ts'
};This will create a "./src/constants/textStrings/index.js" file from which you can import the textstrings. The index.js imports each library and re-exports them using the LibraryName as key. You can also import the desired library directly.
With the singleFile-option only one file will be created. Using <folderPath>.json as fileName (e.g. /src/constants/textStrings.json).
Note: When not using singleFile camel case will be enforced for file name, even when given an alias.
CLI
npx textstrings init
npx textstrings pull
npx textstrings push (-f, --force) (-d, --delete-unused) (--dry-run)Short alias:
npx txt initNote: The old aliases tobit-textstrings and tts have been removed.
Recommended usage
Using a postinstall script which executes the pull command is no longer recommended, because this caused errors when building the project via gitlab without executing push before. Current suggestion executing it is manually.
Still not sure about the best way to execute push-command. Maybe via pre-commit/pre-push/pre-merge hook?
Just running push-command manually whenever you updated/added textstrings might be the best approach though.
Generally you should call the pull-command before editing any textstrings.
textStrings.json format
Notice that you can omit the configured prefix (e.g. txt_chayns_example) in the tree structure
{
"admin": {
"dialog": {
"error": {
"stringName": "txt_chayns_example_admin_dialog_error",
"fallback": "Error"
}
}
}
}When editing the file you can also directly assign the fallback instead of creating an object with stringName and fallback. Also note that the file structure is more important than the stringName, which can also be omitted while editing.
Note: You should execute push-command followed by pull-command before commiting when doing so.
Usage
For usage in your project there would be two options now.
- direct
return <Translation textString="txt_chayns_admin_intro_headline" tagName="h1" />;- lookup file
import textstrings from 'src/constants/textStrings.json';
return <Translation textString={textstrings.intro.headline} tagName="h1" />;Even though the first option seems sufficient, using the lookup file is recommended because it will be auto-generated and is not vulnerable to typos. Also you might lose track of the used textstrings if you don't have a central file which contains all known textstrings.
chayns-components v4/v5
Compare to previous usages with textstring constants file
import textstrings from 'src/constants/textStrings.json';
// well, this seems ok
<Textstring {...textstrings.intro.headline}>
<h1/>
</Textstring>
// but some projects used an object with name and fallback properties instead
// seems redundant already
<Textstring
stringName={textstrings.intro.headline.name}
fallback={textstrings.intro.headline.fallback}
>
<h1/>
</Textstring>
// using the getTextstring function it is even worse
<h1>{Textstring.getTextstring(textstrings.intro.headline.stringName) || textstrings.intro.headline.fallback}</h1>
// 2nd argument is language, but fallback is 3rd argument
// seems odd, right? well, language argument existed first, this was a simple way to maintain backward support (not the best way though)
<h1>{Textstring.getTextstring(textstrings.intro.headline.stringName, undefined, textstrings.intro.headline.fallback)}</h1>
// own getTextstring util saved the day?
const getTextstring = ({ stringName, fallback }, replacements) => {
const text = Textstring.getTextstring(stringName, fallback);
if (!replacements) return text;
// sometimes this even included a custom replacement function
...
};
<h1>{getTextstring(textstrings.intro.headline)}</h1>
// while the above examples refer to chayns-components@4 the v5 has other issues
// 1. all components will be rendered and visible before textstrings are loaded and re-rendered as soon they are loaded
// 2. due to context implementation usage in callbacks (e.g. for dialogs) seems clunky
const headline = useTextstringValue({ textstrings: textstrings.intro.headline });
const description = useTextstringValue({ textstrings: textstrings.intro.description });
... // and all the other textstrings you would potentially use in any dialog
createDialog({
type: DialogType.ALERT,
title: headline,
text: description
}).open()
// what about the replacements if the some variable is not resolved yet because e.g. an api request is necessary first. suddenly you have to do the replacement yourself again (simple and naive example) which is redundant and should not happen
createDialog({
type: DialogType.ALERT,
title: headline,
text: description.replace('##SUBSCRIPTION_END##', new Date(response.subscriptionEnd).toLocaleDateString(getLanguage().active))
}).open();
Versioning of TextStrings
Starting with version 0.7.0, this package supports explicit versioning of TextStrings via the key name.
Versioning is non-structural and must not introduce additional nesting levels.
⚠️ Breaking change
If you previously used versioned TextStrings with a _v<n> suffix and accessed them via an
additional nesting level (e.g. .v2), you must update your code and remove this nesting level.
Rules
1. Version suffix
A TextString is versioned only if its key ends with:
_v<n>
Where n is an integer >= 2 and the suffix is at the very end of the key.
Valid examples:
- txt_example_component_title_v2
- txt_example_component_title_v10
Invalid examples:
- txt_example_component_title_v1
- txt_example_component_title_ipv4
- txt_example_component_title_vpn2
The absence of a version suffix always represents version 1.
2. Object structure
Versioning must not change the object path.
Correct: translations.component.title
Incorrect: translations.component.title.v2
Even if the underlying key contains _v<n>, the object structure remains unchanged.
3. Storage behavior
Version 1:
- stored under the base key
Version >= 2:
- stored with the
_v<n>suffix
Example resolution for: translations.component.title
Possible keys:
- txt_example_component_title
- txt_example_component_title_v2
- txt_example_component_title_v3
4. Reserved suffix
The suffix pattern _v<n> is reserved exclusively for versioning.
It must not be used for other semantics such as IPv4 or IPv6.
Invalid:
- ..._v4 (IPv4)
- ..._v6 (IPv6)
Use explicit alternatives instead, e.g.:
- _ipv4
- _ipv6
Summary
- Versioning is key-based, not structure-based
_v<n>is the only valid version marker- Version 1 is implicit
- Object paths never change due to versioning
Component
Provider
import { Suspense } from 'react';
const libraries = useMemo(() => {
const res = ['<yourLibrary_shared'];
if (isAdminMode) {
res.push('<yourLibrary_admin');
}
}, [isAdminMode])
return (
<Suspense>
<TextStringProvider
libraries={libraries}
>
<App>
</TextStringProvider>
<Suspense>
);TextStringProvider will suspend on mount when initially loading the libraries, but will not suspend when loading additional libraries or changing the language. TextStrings will then use the previous language until the new language finished loading.
Suspense is handled internally and you no longer need to add a Suspense-Boundary yourself when using v0.5.0-beta.0 or higher.
For usage without chayns-api you can also use the TextStringProviderBase which does not have the dependency to chayns-api and thus also does not support some features like automatic language detection or opening the edit dialog.
Translation
import { txtPersonsAdmin } from '/src/constants/textstrings'
const stringName = 'txt_example';
// just renders the text without surrounding html-tag (default is span)
// since there is no html-element to assign a event-listener to this will not have the CTRL+click listener
return (
<Translation textString={stringName} tagName={null}/>
);
// you can either pass the stringName without fallback directly or an object with stringName and fallback (optional)
return (
<>
<Translation textString={txtPersonsAdmin.admin.error.alert}/>
<Translation textString={txtPersonsAdmin.admin.error.alert.stringName}/>
</>
);
// as above with replacements
return (
<Translation textString={stringName} replacements={{ 'COUNT': 2 }}/>
);
// renders a span with the text
// this will have the CTRL+click listener
return (
<Translation textString={stringName}/>
);
// using the hook
const { t } = useTranslation();
...
createDialog({
type: DialogType.ALERT,
text: t(stringName, { 'COUNT': 2 }),
}).open();
createDialog({
type: DialogType.ALERT,
text: t(txtPersonAdmin.admin.error.alert, { 'COUNT': 2 }),
}).open()
// outside react context, getFixedT behaves same as t-function but is bound to the default store
import { getFixedT } from '@chayns/textstrings';
createDialog({
type: DialogType.ALERT,
text: getFixedT(txtPersonAdmin.admin.error.alert, { 'COUNT': 2 }),
}).open()
// renderProp
return (
<Translation>
{(t, onClick) => <div onClick={onClick}>{t(stringName)}</div>}
</Translation>
)
When you have to use dangerouslySetInnerHTML either use the renderProp or the hook. The lack of a "useDangerousSetInnerHTML" (v4) or "isHTML" (v5) prop is an intentional design choice. While the first still includes the hint that this is dangerous according to the design choice by react, the latter looks like it is safe to use.
If possible try to avoid using textstrings which require dangerouslySetInnerHTML. Also check the documentation to dangerouslySetInnerHTML if you are not familiar with the security risks.
const stringName = 'txt_example';
// using hook. be aware that this will not have the CTRL+click handler
const { t } = useTranslation();
return <div dangerouslySetInnerHTML={{ __html: t(stringName) }} />;
// using renderProp. this will have the CTRL+click but only if you pass the onClick
return <Translation textString={stringName}>{(text, onClick) => <div dangerouslySetInnerHTML={{ __html: text }} onClick={onClick} />}</Translation>;Replacements
When using replacements the hashtags are automatically added now. Instead of { '##count##': 1 } you can simply write { count: 1 } and this will replace ##count## in the text. Explicitly adding the hashtags in the replacement object will not work though.
Plurals
New handling for plurals has been added and gives count special meaning in replacements. You can define separate textstrings for counts of zero and one. The used textstring will the automatically determined by the count.
{
"zero": {
"stringName": "YOUR_PREFIX_NAME_zero",
"fallback": "Keine Person"
},
"one": {
"stringName": "YOUR_PREFIX_NAME_one",
"fallback": "Eine Person"
},
"other": {
"stringName": "YOUR_PREFIX_NAME_other",
"fallback": "##count## Personen"
}
}Formatting
When passing numbers in replacements those will be formatted with toLocaleString by default (without options). Besides count you could format those yourself or pass formatOptions.
<Translation
textString={YOUR_TEXTSTRING}
replacements={{ price: { value: 5.24, formatOptions: { style: 'currency', currency: 'EUR' } } }}
/>Nesting
Also support for nested textstrings has been added.
Basic example:
{
"nesting1": "1 $t(nesting2)",
"nesting2": "2 $t(nesting3)",
"nesting3": "3"
}Example with options:
{
"outer": {
"stringName": "YOUR_PREFIX_NAME_nesting",
"fallback": "Sie haben $t(\"YOUR_PREFIX_NAME_boys\", { \"count\": \"boys\" }) und $t(\"YOUR_PREFIX_NAME_girls\", { \"count\": \"girls\" })"
},
"boys": {
"stringName": "YOUR_PREFIX_NAME_boys",
"fallback": "##count## Jungs"
},
"girls": {
"stringName": "YOUR_PREFIX_NAME_girls",
"fallback": "##count## Mädchen"
}
}The above example has been simplified by not using different textstrings depending on counts but that is possible too.
React Replace
Replacements can only be strings/numbers and no components. In case you have a textString with a link which requires a
custom onClick-handling you can use the TranslationReactReplace-component.
Assume following result as example:
Lorem ipsum. <a onClick={selectPage({ id: 93 })}>Hier klicken</a> für weitere Informationen.
Create two textStrings e.g. like this:
linkOuter: Lorem ipsum. ##link## für weitere Informationen.
linkInner: Hier klickenimport { TranslationReactReplace } from '@chayns/textstrings';
return (
<TranslationReactReplace
textString="linkOuter"
regex="##link##"
>
{(match) => (
<a key={match} onClick={() => selectPage({ id: 93 })}>
<Translation textString="linkInner"/>
</a>
)}
</TranslationReactReplace>
);Make sure to give components returned in the renderProp a key. Also when using a regex it is important that it has exactly one capture group. However, you may use as many non-capturing groups as you want.
"##link##" is basically the same as /(##link##)/.
For multiple replacements you will need to use a RegExp like /(##link1##|##link2##)/, /##(link1|link2)##/ or /##(link[01])##/. Be aware parts which are not part of the capture group will not be passed as match to the renderProp.
If you only need exactly one replacement you could also pass the replacement directly as child. Note: This will only replace the first match, all other matches will be replaced with null.
import { TranslationReactReplace } from '@chayns/textstrings';
return (
<TranslationReactReplace
textString="linkOuter"
regex="##link##"
>
<a onClick={() => selectPage({ id: 93 })}>
<Translation textString="linkInner"/>
</a>
</TranslationReactReplace>
);Server-side Rendering (SSR)
Example code for usage in SSR. The TextStringProviderSSR will render the TextStringProvider and make sure that the correct language is loaded during SSR.
Requires at least [email protected] which added the 4th parameter in the withHydrationBoundary-HOC.
Without useHydrationId
import { TextStringProviderSSR } from '@chayns/textstrings';
return (
<ChaynsProvider>
...
<TextStringProviderSSR id="<your project name or unique id>/textStrings" libraries={libraries}>
{children}
</TextStringProviderSSR>
...
</ChaynsProvider>
);With useHydrationId
import { withTextStringProviderSSR } from '@chayns/textstrings';
const useHydrationId = () => {
const { id } = useCustomData();
return `<your project name or unique id>/${id}`;
};
const TextStringProviderSSR = withTextStringProviderSSR(useHydrationId);
return (
<ChaynsProvider>
...
<TextStringProviderSSR libraries={libraries}>{children}</TextStringProviderSSR>
...
</ChaynsProvider>
);Publishing
To publish the package push your changes and merge to main.
Use npm version patch|minor|major|prerelease [--preid beta] on main branch.
Push the generated commit and also push the tag to trigger build_publish via gitlab-ci.
