Skip to content

Commit

Permalink
feat(frontend): add getTranslation() function (#245)
Browse files Browse the repository at this point in the history
* feat(fronend): add `getTranslation()` function
* feat(fronend): use `getTranslation()` function
  • Loading branch information
gregory-j-baker authored Feb 21, 2025
1 parent 10764f4 commit da4660b
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 64 deletions.
23 changes: 23 additions & 0 deletions frontend/app/i18n-config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getLanguage } from '~/utils/i18n-utils';
* @param languageOrRequest - The language code or Request object to get the language from.
* @param namespace - The namespace to get the translation function for.
* @returns A translation function for the given language and namespace.
* @throws {AppError} If no language is found in the `languageOrRequest`.
*/
export async function getFixedT<NS extends Namespace>(
languageOrRequest: Language | Request,
Expand All @@ -34,6 +35,28 @@ export async function getFixedT<NS extends Namespace>(
return i18n.getFixedT(language, namespace);
}

/**
* Similar to react-i18next's `useTranslation()` hook, this function will return a `t`
* function and a `lang` value that represents the current language.
*
* @param languageOrRequest - The language code or Request object to get the language from.
* @param namespace - The namespace to get the translation function for.
* @returns A Promise resolving to an object containing the language code (`lang`) and a translation function (`t`) for the given namespace.
* @throws {AppError} If no language is found in the `languageOrRequest`.
*/
export async function getTranslation<NS extends Namespace>(
languageOrRequest: Language | Request,
namespace: NS,
): Promise<{ lang: Language; t: TFunction<NS> }> {
const lang = getLanguage(languageOrRequest);

if (lang === undefined) {
throw new AppError('No language found in request', ErrorCodes.NO_LANGUAGE_FOUND);
}

return { lang, t: await getFixedT(languageOrRequest, namespace) };
}

/**
* Creates and initializes an i18next instance for server-side rendering.
*/
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/routes/protected/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Route } from './+types/admin';

import { requireAuth } from '~/.server/utils/auth-utils';
import { PageTitle } from '~/components/page-title';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';

export const handle = {
Expand All @@ -15,7 +15,7 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['admin']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);
return { documentTitle: t('protected:index.page-title') };
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/app/routes/protected/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { requireAuth } from '~/.server/utils/auth-utils';
import { Card, CardDescription, CardHeader, CardIcon, CardTitle } from '~/components/card';
import { AppLink } from '~/components/links';
import { PageTitle } from '~/components/page-title';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import type { I18nRouteFile } from '~/i18n-routes';
import { handle as parentHandle } from '~/routes/protected/layout';

Expand All @@ -23,7 +23,7 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);
return { documentTitle: t('protected:index.page-title') };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useId } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle, SessionData } from 'react-router';
import { data, useFetcher } from 'react-router';

import { useTranslation } from 'react-i18next';
import * as v from 'valibot';
Expand All @@ -19,9 +19,8 @@ import { InputRadios } from '~/components/input-radios';
import { InputSelect } from '~/components/input-select';
import { PageTitle } from '~/components/page-title';
import { AppError } from '~/errors/app-error';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';

type ContactInformationSessionData = NonNullable<SessionData['inPersonSINCase']['contactInformation']>;

Expand All @@ -33,8 +32,7 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);
const { lang, t } = await getTranslation(request, handle.i18nNamespace);

return {
documentTitle: t('protected:contact-information.page-title'),
Expand All @@ -49,9 +47,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');

Expand Down
19 changes: 11 additions & 8 deletions frontend/app/routes/protected/person-case/current-name.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { ChangeEvent } from 'react';
import { useId, useState } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle } from 'react-router';
import { data, useFetcher } from 'react-router';

import { faExclamationCircle, faXmark } from '@fortawesome/free-solid-svg-icons';
import type { SessionData } from 'express-session';
import { useTranslation } from 'react-i18next';
import * as v from 'valibot';

import type { Route, Info } from './+types/current-name';
import type { Info, Route } from './+types/current-name';

import { requireAuth } from '~/.server/utils/auth-utils';
import { i18nRedirect } from '~/.server/utils/route-utils';
Expand All @@ -23,15 +23,17 @@ import { PageTitle } from '~/components/page-title';
import { Progress } from '~/components/progress';
import { AppError } from '~/errors/app-error';
import { ErrorCodes } from '~/errors/error-codes';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';
import { REGEX_PATTERNS } from '~/utils/regex-utils';
import { trimToUndefined } from '~/utils/string-utils';

type CurrentNameSessionData = NonNullable<SessionData['inPersonSINCase']['currentNameInfo']>;

const REQUIRE_OPTIONS = { yes: 'Yes', no: 'No' } as const;
const REQUIRE_OPTIONS = {
yes: 'Yes', //
no: 'No',
} as const;

const VALID_DOC_TYPES: string[] = [
'marriage-document',
Expand All @@ -51,9 +53,11 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);

const { t } = await getTranslation(request, handle.i18nNamespace);
const currentNameInfo = context.session.inPersonSINCase?.currentNameInfo;
const preferredSameAsDocumentName = currentNameInfo?.preferredSameAsDocumentName;

return {
documentTitle: t('protected:primary-identity-document.page-title'),
defaultFormValues: {
Expand Down Expand Up @@ -83,9 +87,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');
const nameMaxLength = 100;
Expand Down
13 changes: 6 additions & 7 deletions frontend/app/routes/protected/person-case/previous-sin.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useId, useState } from 'react';
import type { ChangeEvent } from 'react';
import { useId, useState } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle, SessionData } from 'react-router';
import { data, useFetcher } from 'react-router';

import { useTranslation } from 'react-i18next';
import * as v from 'valibot';
Expand All @@ -17,9 +17,8 @@ import { InputPatternField } from '~/components/input-pattern-field';
import { InputRadios } from '~/components/input-radios';
import { PageTitle } from '~/components/page-title';
import { AppError } from '~/errors/app-error';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';
import { formatSin, isValidSin, sinInputPatternFormat } from '~/utils/sin-utils';

type PreviousSinSessionData = NonNullable<SessionData['inPersonSINCase']['previousSin']>;
Expand All @@ -32,7 +31,8 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);

return {
documentTitle: t('protected:previous-sin.page-title'),
defaultFormValues: context.session.inPersonSINCase?.previousSin,
Expand All @@ -45,9 +45,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');

Expand Down
19 changes: 6 additions & 13 deletions frontend/app/routes/protected/person-case/primary-docs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useId, useState } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle } from 'react-router';
import { data, useFetcher } from 'react-router';

import { faExclamationCircle, faXmark } from '@fortawesome/free-solid-svg-icons';
import type { SessionData } from 'express-session';
import { useTranslation } from 'react-i18next';
import * as v from 'valibot';

import type { Route, Info } from './+types/primary-docs';
import type { Info, Route } from './+types/primary-docs';

import { requireAuth } from '~/.server/utils/auth-utils';
import { i18nRedirect } from '~/.server/utils/route-utils';
Expand All @@ -19,19 +19,13 @@ import { PageTitle } from '~/components/page-title';
import { Progress } from '~/components/progress';
import { AppError } from '~/errors/app-error';
import { ErrorCodes } from '~/errors/error-codes';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';

type PrimaryDocumentsSessionData = NonNullable<SessionData['inPersonSINCase']['primaryDocuments']>;

/**
* Valid current status in Canada for proof of concept
*/
const VALID_CURRENT_STATUS = ['canadian-citizen-born-outside-canada'];
/**
* Valid document type for proof of concept
*/

const VALID_DOCTYPE = ['certificate-of-canadian-citizenship', 'certificate-of-registration-of-birth-abroad'];

export const handle = {
Expand All @@ -40,7 +34,7 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);

return {
documentTitle: t('protected:primary-identity-document.page-title'),
Expand All @@ -54,9 +48,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');

Expand Down
14 changes: 6 additions & 8 deletions frontend/app/routes/protected/person-case/privacy-statement.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useId } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle } from 'react-router';
import { data, useFetcher } from 'react-router';

import { faXmark, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { faExclamationCircle, faXmark } from '@fortawesome/free-solid-svg-icons';
import type { SessionData } from 'express-session';
import { useTranslation } from 'react-i18next';
import * as v from 'valibot';

import type { Route, Info } from './+types/privacy-statement';
import type { Info, Route } from './+types/privacy-statement';

import { requireAuth } from '~/.server/utils/auth-utils';
import { i18nRedirect } from '~/.server/utils/route-utils';
Expand All @@ -18,9 +18,8 @@ import { InputCheckbox } from '~/components/input-checkbox';
import { PageTitle } from '~/components/page-title';
import { Progress } from '~/components/progress';
import { AppError } from '~/errors/app-error';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';

type PrivacyStatmentSessionData = NonNullable<SessionData['inPersonSINCase']['privacyStatement']>;

Expand All @@ -30,7 +29,7 @@ export const handle = {

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);

return {
documentTitle: t('protected:privacy-statement.page-title'),
Expand All @@ -44,9 +43,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');

Expand Down
25 changes: 12 additions & 13 deletions frontend/app/routes/protected/person-case/request-details.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useId } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle, SessionData } from 'react-router';
import { data, useFetcher } from 'react-router';

import { useTranslation } from 'react-i18next';
import * as v from 'valibot';
Expand All @@ -16,15 +16,11 @@ import { InputRadios } from '~/components/input-radios';
import { InputSelect } from '~/components/input-select';
import { PageTitle } from '~/components/page-title';
import { AppError } from '~/errors/app-error';
import { getFixedT } from '~/i18n-config.server';
import { getTranslation } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';

type RequestDetailsSessionData = NonNullable<SessionData['inPersonSINCase']['privacyStatement']>;

/**
* Valid requests when creating a request
*/
const VALID_REQUESTS = [
'first-time',
'record-confirmation',
Expand All @@ -35,18 +31,22 @@ const VALID_REQUESTS = [
'new-sin',
] as const;

/**
* Valid scenarios when creating a request
*/
const VALID_SCENARIOS = ['for-self', 'legal-guardian', 'legal-representative', 'as-employee', 'estate-representative'] as const;
const VALID_SCENARIOS = [
'for-self', //
'legal-guardian',
'legal-representative',
'as-employee',
'estate-representative',
] as const;

export const handle = {
i18nNamespace: [...parentHandle.i18nNamespace, 'protected'],
} as const satisfies RouteHandle;

export async function loader({ context, request }: Route.LoaderArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const t = await getFixedT(request, handle.i18nNamespace);
const { t } = await getTranslation(request, handle.i18nNamespace);

return {
documentTitle: t('protected:request-details.page-title'),
defaultFormValues: context.session.inPersonSINCase?.requestDetails,
Expand All @@ -59,9 +59,8 @@ export function meta({ data }: Route.MetaArgs) {

export async function action({ context, request }: Route.ActionArgs) {
requireAuth(context.session, new URL(request.url), ['user']);
const lang = getLanguage(request);
const t = await getFixedT(request, handle.i18nNamespace);

const { lang, t } = await getTranslation(request, handle.i18nNamespace);
const formData = await request.formData();
const action = formData.get('action');

Expand Down
Loading

0 comments on commit da4660b

Please sign in to comment.