Skip to content

Commit 23f01c9

Browse files
committed
I18Next: Add section about usage to Readme.md
1 parent d215be0 commit 23f01c9

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,203 @@ It should be noted, that the order of classnames given to component does not aut
475475
- Should be used as the final step when combining className lists and passing them
476476
onto raw HTML nodes or external React components.
477477

478+
### I18Next
479+
480+
`18next` & `i18next-react` is used to manage and handle all user facing strings in the UI.
481+
Examples of good and bad code in relation to translations below. Many if the following are
482+
more general guidelines to translation handling in any and all software project. This project
483+
only truly uses Finnish as the language, providing a rudimentary English translation also.
484+
So some of these could be ignored in Jore, due to the limited set of languages in use, but
485+
why learn bad habits or broken patterns that can cause problems in your future projects?
486+
487+
```tsx
488+
import type { SelectorKey, SelectorParam, TFunction } from 'i18next';
489+
import { Trans, useTranslation } from 'i18next-react';
490+
491+
/* translations.json
492+
* {
493+
* // Good strings
494+
* myString: 'My translation string',
495+
* someEnumA: 'This the A value of SomeEnum',
496+
* someEnumB: 'This the B value of SomeEnum',
497+
*
498+
* // Plurals are marked by duplicating the translation string and marking the plural with __other postfix.
499+
* // If translating to a more complex language see:
500+
* // https://www.i18next.com/translation-function/plurals#languages-with-multiple-plurals
501+
* routeCount: '1 route', // Or: {{count}} route
502+
* routeCount__other: '{{count}} routes',
503+
*
504+
* // Keep string "fragments" scoped within the use case.
505+
* // I.e. random translations should not be injected into other translations,
506+
* // unless it is clear that they belong together and function well.
507+
* scopedStopCountOnRoute: {
508+
* stopCount: "1 pysäkki",
509+
* stopCount__other: "{{count}} pysäkkiä",
510+
* busRouteStopCount: "Bussireittillä {{routeLabel}} on $t(scopedStopCountOnRoute.stopCount, { \"count\": {{stopCount}} }).",
511+
* tramRouteStopCount: "Ratikkareitillä {{routeLabel}} on $t(scopedStopCountOnRoute.stopCount, { \"count\": {{stopCount}} }).",
512+
* },
513+
* compelexLongText: "Instructional text about color use in <b>Jore</b>. <BusColor>This color is used for busses in Jore<BusColor>, <TramColor>...</TramColor>. See <LinkToDocumentation>Color guide</LinkToDocumentation> for more indetail instructrions."
514+
*
515+
*
516+
* // Bad strings
517+
* routeCountOne: '1 route'
518+
* routeCountPlural: '{{count}} routes'
519+
*
520+
* // Defined on line 200. Last change: 2024-01-31: Decapitalize route type on RoutePage
521+
* routeType: {
522+
* bus: 'bussireitti',
523+
* tram: 'ratikkareitti'
524+
* },
525+
* // ... 2000 other translations here
526+
* // Defined on line 2500. Last change: 2026-04-02: Make text more fluid on StopPage
527+
* stopCount: "Pysäkkien lukumäärä: {{count}}",
528+
* // Defined on line 4000. Last change: 2022-05-05: Add nice Route header line to LinePage
529+
* stopCountOnRoute: "{{type}}llä {{routeLabel}} on $t(stopCount, { \"count\": {{stopCount}} })."
530+
*
531+
* compelexTextFragments: {
532+
* intro: "Instructional text about color use in",
533+
* jore: "Jore",
534+
* bus: "This color is used for busses in Jore",
535+
* tram: "...",
536+
* see: "See",
537+
* link: "Color guide",
538+
* more: "for more indetail instructrions."
539+
* }
540+
* }
541+
*/
542+
543+
// Always stay within React's reactive context.
544+
// Access i18next trough the:
545+
const { t, i18n } = useTranslation(); // Hook
546+
547+
// Use the new Typed selectorApi to choose strings
548+
const goodSelection = t(($) => $.myString);
549+
const badSelection = t('myString'); // Won't even compile anymore, with selector API activated
550+
551+
// If you need to access translations outside of React component or hook, either:
552+
// Pass troug the t-function instance into the function, from the calling React context.
553+
function passInT(t: TFunction, value: SomeEnum): ReactNode {
554+
switch (value) {
555+
case SomeEnum.A:
556+
return t(($) => $.someEnumA);
557+
case SomeEnum.B:
558+
return t(($) => $.someEnumB);
559+
}
560+
}
561+
const passInTUsed = passInT(t, SomeEnum.A);
562+
563+
// Or return a SelectorFunction from the function
564+
function returnSelector(value: SomeEnum): SelectorParam {
565+
switch (value) {
566+
case SomeEnum.A:
567+
return ($) => $.someEnumA;
568+
case SomeEnum.B:
569+
return ($) => $.someEnumB;
570+
}
571+
}
572+
const returnSelectorUsed = t(returnSelector(SomeEnum.A));
573+
574+
// Technicallly it is also possible to construct and return SelectorKey
575+
// directly from a function to be passed into t. No example of that.
576+
577+
// Prefer to keep the complex logick outside of the SelectorFunction
578+
// Good examples: returnSelector
579+
// Bad code:
580+
function badReturnComplexSelector(value: SomeEnum): SelectorParam {
581+
return ($) => {
582+
switch (value) {
583+
case SomeEnum.A:
584+
$.someEnumA;
585+
case SomeEnum.B:
586+
$.someEnumB;
587+
}
588+
};
589+
}
590+
591+
// Trust I18next to handle plurals:
592+
const goodPlural = t(($) => $.routeCount, { count: routes.length });
593+
const badPlural =
594+
routes.length === 1
595+
? t(($) => $.routeCountOne)
596+
: t(($) => $.routeCountPlural, { count: routes.length });
597+
598+
// Prefer having full text strings in the translation file,
599+
// instead of constructing them from multiple parts in the UI code.
600+
// Grammar and natural language constructs should be encoded within the
601+
// the translations, and not being handled in the ts code.
602+
function trGoodStopCountOnRoute(
603+
t: TFunction,
604+
routeType: RouteType,
605+
routeLabel: string,
606+
stopCount: number,
607+
): ReactNode {
608+
switch (routeType) {
609+
case RouteType.Bus:
610+
return t(($) => $.scopedStopCountOnRoute.busRouteStopCount, {
611+
routeLabel,
612+
stopCount,
613+
});
614+
// ...
615+
}
616+
}
617+
trGoodStopCountOnRoute(t, RouteType.Bus, '123', 10);
618+
// == Bussireitillä 123 on 10 pysäkkiä.
619+
620+
function trBadStopCountOnRoute(
621+
t: TFunction,
622+
routeType: RouteType,
623+
routeLabel: string,
624+
stopCount: number,
625+
): ReactNode {
626+
switch (routeType) {
627+
case RouteType.Bus:
628+
return t(($) => $.stopCountOnRoute, {
629+
type: t(($) => $.routeType.bus),
630+
routeLabel,
631+
stopCount,
632+
});
633+
// ...
634+
}
635+
}
636+
trBadStopCountOnRoute(t, RouteType.Bus, '123', 10);
637+
// == bussireittillä 123 on Pysäkkien lukumäärä: 10.
638+
639+
// Good complex long text sections:
640+
const goodCompelexLongText = (
641+
<p>
642+
<Trans
643+
i18nKey={($) => $.compelexLongText}
644+
components={{
645+
BusColor: <span className="color-bus" />,
646+
TramColor: <span className="color-tram" />,
647+
LinkToDocumentation: (
648+
<a href="http://jore4.hsl.fi/documentation/colors" />
649+
),
650+
}}
651+
/>
652+
</p>
653+
);
654+
655+
// Bad complex long text section constructed from pieces on the code side:
656+
const badCompelexLongText = (
657+
<p>
658+
{t(($) => $.compelexTextFragments.intro)}{' '}
659+
<b>{t(($) => $.compelexTextFragments.intro)}</b>
660+
{'. '}
661+
<span className="color-bus">
662+
{t(($) => $.compelexTextFragments.bus)}
663+
</span>{' '}
664+
<span className="color-tram">{t(($) => $.compelexTextFragments.tram)}</span>
665+
{'. '}
666+
{t(($) => $.compelexTextFragments.see)}{' '}
667+
<a href="http://jore4.hsl.fi/documentation/colors">
668+
{t(($) => $.compelexTextFragments.link)}
669+
</a>{' '}
670+
{t(($) => $.compelexTextFragments.more)}
671+
</p>
672+
);
673+
```
674+
478675
## Loading state of async request handling / indication
479676

480677
To have consistency and reduce duplicated code and passing loading states through components we use redux for the loading state of async requests. We have `enum Operation` in `loader.ts` which contains different async operations e.g. `ConfirmTimetablesImport`. When making an async request, you can use the `setLoadingAction` with the correct `Operation` to handle the state. After this you can use the state how you please, but if you want to show the global `LoadingOverlay`, just add the chosen `Operation` to `joreOperations` in `loader.ts`.

0 commit comments

Comments
 (0)