Skip to content

Commit

Permalink
fixes validation message formatting/styling and adds smaller changes …
Browse files Browse the repository at this point in the history
…to useLanguage hook (#2098)

* fixes validation message formatting/styling and adds smaller changes to useLanguage hook

* updates language selector test by getting loaded text from resources. Prevents race condition
  • Loading branch information
cammiida committed May 16, 2024
1 parent da9d106 commit 7489f30
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 84 deletions.
4 changes: 2 additions & 2 deletions src/components/form/SoftValidations.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { useLanguage } from 'src/features/language/useLanguage';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';
import { AlertBaseComponent } from 'src/layout/Alert/AlertBaseComponent';

export interface ISoftValidationProps {
Expand All @@ -12,7 +12,7 @@ export interface ISoftValidationProps {
export type SoftValidationVariant = 'warning' | 'info' | 'success';

export const validationMessagesToList = (message: string, index: number) => (
<li key={`validationMessage-${index}`}>{getParsedLanguageFromText(message)}</li>
<li key={`validationMessage-${index}`}>{parseAndCleanText(message)}</li>
);

export function SoftValidations({ variant, errorMessages }: ISoftValidationProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button, Checkbox, Heading, Label, Paragraph } from '@digdir/designsyste
import classes from 'src/features/devtools/components/FeatureToggles/FeatureToggles.module.css';
import { SplitView } from 'src/features/devtools/components/SplitView/SplitView';
import { getAugmentedFeatures } from 'src/features/toggles';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';
import type { FeatureToggleSource, IFeatureToggles } from 'src/features/toggles';

const sourceMap: { [key in FeatureToggleSource]: string } = {
Expand Down Expand Up @@ -64,7 +64,7 @@ export function FeatureToggles() {
size={'small'}
level={4}
>
{getParsedLanguageFromText(title)}
{parseAndCleanText(title)}
</Heading>
<Label size={'xsmall'}>Nøkkel: {key}</Label>
<br />
Expand All @@ -74,7 +74,7 @@ export function FeatureToggles() {
<br />
<Label size={'xsmall'}>Kilde: {sourceMap[source]}</Label>
<Paragraph>
{getParsedLanguageFromText(description)}
{parseAndCleanText(description)}
{links && links.length && (
<ul className={classes.linkList}>
{links.map((url) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useDevToolsStore } from 'src/features/devtools/data/DevToolsStore';
import { useLayoutValidationForPage } from 'src/features/devtools/layoutValidation/useLayoutValidation';
import { useLayouts, useLayoutSetId } from 'src/features/form/layout/LayoutsContext';
import { useCurrentView } from 'src/hooks/useNavigatePage';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';
import { useNodes } from 'src/utils/layout/NodesContext';
import type { LayoutContextValue } from 'src/features/form/layout/LayoutsContext';

Expand Down Expand Up @@ -130,7 +130,7 @@ export const LayoutInspector = () => {
<div className={classes.errorList}>
<ul>
{validationErrorsForPage[selectedComponent].map((error) => (
<li key={error}>{getParsedLanguageFromText(error)}</li>
<li key={error}>{parseAndCleanText(error)}</li>
))}
</ul>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/features/language/useLanguage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { screen } from '@testing-library/react';

import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';
import { renderWithMinimalProviders, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders';

const TestElementAsString = ({ input }: { input: string }) => {
const { elementAsString } = useLanguage();
return <div data-testid='subject'>{elementAsString(getParsedLanguageFromText(input))}</div>;
return <div data-testid='subject'>{elementAsString(parseAndCleanText(input))}</div>;
};

const TestSimple = ({ input }: { input: string }) => {
Expand Down
132 changes: 68 additions & 64 deletions src/features/language/useLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { FD } from 'src/features/formData/FormDataWrite';
import { Lang } from 'src/features/language/Lang';
import { useLangToolsDataSources } from 'src/features/language/LangToolsStore';
import { getLanguageFromCode } from 'src/language/languages';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';
import { useFormComponentCtx } from 'src/layout/FormComponentContext';
import { getKeyWithoutIndexIndicators } from 'src/utils/databindings';
import { transposeDataBinding } from 'src/utils/databindings/DataBinding';
import { smartLowerCaseFirst } from 'src/utils/formComponentUtils';
import type { useDataModelReaders } from 'src/features/formData/FormDataReaders';
import type { TextResourceMap } from 'src/features/language/textResources';
import type { FixedLanguageList } from 'src/language/languages';
import type { FixedLanguageList, NestedTexts } from 'src/language/languages';
import type { FormDataSelector } from 'src/layout';
import type { IApplicationSettings, IInstanceDataSources, ILanguage, IVariable } from 'src/types/shared';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
Expand Down Expand Up @@ -119,63 +119,18 @@ interface ILanguageState {
dataSources: TextResourceVariablesDataSources;
}

/**
* Static version, like the above and below functions, but with an API that lets you pass just the state you need.
* This is useful for testing, but please do not use this in production code (where all arguments should be passed,
* even if the signature is updated).
*/
export function staticUseLanguageForTests({
textResources = {},
language = null,
selectedLanguage = 'nb',
dataSources = {
instanceDataSources: {
instanceId: 'instanceId',
appId: 'org/app',
instanceOwnerPartyId: '12345',
instanceOwnerPartyType: 'person',
},
dataModels: new DataModelReaders({}),
currentDataModelName: undefined,
currentDataModel: () => null,
applicationSettings: {},
node: undefined,
},
}: Partial<ILanguageState> = {}) {
return staticUseLanguage(textResources, language, selectedLanguage, dataSources);
}

export function staticUseLanguage(
textResources: TextResourceMap,
_language: ILanguage | null,
selectedLanguage: string,
dataSources: TextResourceVariablesDataSources,
): IUseLanguage {
const language = _language || getLanguageFromCode(selectedLanguage);
const lang: IUseLanguage['lang'] = (key, params) => {
const result = getUnprocessedTextValueByLanguage(key, params);

function base(
key: string | undefined,
params?: ValidLangParam[],
extendedSources?: Partial<TextResourceVariablesDataSources>,
processing = true,
) {
if (!key) {
return '';
}

const textResource = getTextResourceByKey(key, textResources, { ...dataSources, ...extendedSources });
if (textResource !== key) {
// TODO(Validation): Use params if exists and only if no variables are specified (maybe add datasource params to variables definition)
return processing ? getParsedLanguageFromText(textResource) : textResource;
}

const name = getLanguageFromKey(key, language);
const out = params ? replaceParameters(name, simplifyParams(params, langAsString)) : name;

return processing ? getParsedLanguageFromText(out) : out;
}

const lang: IUseLanguage['lang'] = (key, params) => base(key, params);
return parseAndCleanText(result);
};

const langAsString: IUseLanguage['langAsString'] = (key, params, makeLowerCase) => {
const postProcess = makeLowerCase ? smartLowerCaseFirst : (str: string | undefined) => str;
Expand All @@ -193,7 +148,7 @@ export function staticUseLanguage(
dataModelPath,
params,
) => {
const result = base(key, params, { dataModelPath });
const result = parseAndCleanText(getUnprocessedTextValueByLanguage(key, params, { dataModelPath }));
if (result === undefined || result === null) {
return key || '';
}
Expand All @@ -202,13 +157,34 @@ export function staticUseLanguage(
};

const langAsNonProcessedString: IUseLanguage['langAsNonProcessedString'] = (key, params) =>
base(key, params, undefined, false);
getUnprocessedTextValueByLanguage(key, params, undefined);

const langAsNonProcessedStringUsingPathInDataModel: IUseLanguage['langAsNonProcessedStringUsingPathInDataModel'] = (
key,
dataModelPath,
params,
) => base(key, params, { dataModelPath }, false);
) => getUnprocessedTextValueByLanguage(key, params, { dataModelPath });

function getUnprocessedTextValueByLanguage(
key: string | undefined,
params?: ValidLangParam[],
extendedSources?: Partial<TextResourceVariablesDataSources>,
) {
if (!key) {
return '';
}

const textResource = getTextResourceByKey(key, textResources, { ...dataSources, ...extendedSources });

if (textResource !== key) {
// TODO(Validation): Use params if exists and only if no variables are specified (maybe add datasource params to variables definition)
return textResource;
}

const name = getLanguageSpecificText(key, language);

return params ? replaceParameters(name, simplifyParams(params, langAsString)) : name;
}

return {
language,
Expand Down Expand Up @@ -254,13 +230,20 @@ const getPlainTextFromNode = (node: ReactNode, langAsString: IUseLanguage['langA
return text;
};

export function getLanguageFromKey(key: string, language: ILanguage) {
function getLanguageSpecificText(key: string, language: ILanguage) {
const path = key.split('.');
const value = getNestedObject(language, path);
if (!value || typeof value === 'object') {
return key;
if (typeof value === 'string') {
return value;
}
return value;
return key;
}

function getNestedObject(nestedObj: ILanguage | Record<string, string | ILanguage> | NestedTexts, pathArr: string[]) {
return pathArr.reduce<ILanguage | string | NestedTexts | undefined>(
(obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined),
nestedObj,
);
}

function getTextResourceByKey(
Expand Down Expand Up @@ -365,12 +348,7 @@ function replaceVariables(text: string, variables: IVariable[], dataSources: Tex

return out;
}

function getNestedObject(nestedObj: ILanguage, pathArr: string[]) {
return pathArr.reduce((obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), nestedObj);
}

const replaceParameters = (nameString: string | undefined, params: SimpleLangParam[]) => {
const replaceParameters = (nameString: string, params: SimpleLangParam[]) => {
if (nameString === undefined) {
return nameString;
}
Expand Down Expand Up @@ -402,3 +380,29 @@ function isTextReference(obj: any): obj is TextReference {
Object.keys(obj).every((k) => k === 'key' || k === 'params' || k === 'makeLowerCase')
);
}

/**
* Static version, like the above and below functions, but with an API that lets you pass just the state you need.
* This is useful for testing, but please do not use this in production code (where all arguments should be passed,
* even if the signature is updated).
*/
export function staticUseLanguageForTests({
textResources = {},
language = null,
selectedLanguage = 'nb',
dataSources = {
instanceDataSources: {
instanceId: 'instanceId',
appId: 'org/app',
instanceOwnerPartyId: '12345',
instanceOwnerPartyType: 'person',
},
dataModels: new DataModelReaders({}),
currentDataModelName: undefined,
currentDataModel: () => null,
applicationSettings: {},
node: undefined,
},
}: Partial<ILanguageState> = {}) {
return staticUseLanguage(textResources, language, selectedLanguage, dataSources);
}
3 changes: 3 additions & 0 deletions src/language/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { nb } from 'src/language/texts/nb';
import { nn } from 'src/language/texts/nn';

export type FixedLanguageList = ReturnType<typeof en>;
export interface NestedTexts {
[n: string]: string | NestedTexts;
}

// This makes sure we don't generate a new object
// each time (which would fail shallow comparisons, in for example React.memo)
Expand Down
6 changes: 3 additions & 3 deletions src/language/sharedLanguage.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { parseAndCleanText } from 'src/language/sharedLanguage';

describe('sharedLanguage.ts', () => {
describe('getParsedLanguageFromText', () => {
it('should return single element if only text is parsed', () => {
const result = getParsedLanguageFromText('just som plain text');
const result = parseAndCleanText('just som plain text');
expect(result instanceof Array).toBeFalsy();
});

it('should return array of nodes for more complex markdown', () => {
const result = getParsedLanguageFromText('# Header \n With some text');
const result = parseAndCleanText('# Header \n With some text');
expect(result instanceof Array).toBeTruthy();
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/language/sharedLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
}
});

export const getParsedLanguageFromText = cachedFunction(
export const parseAndCleanText = cachedFunction(
(text: string | undefined) => {
if (typeof text !== 'string') {
return null;
Expand Down
6 changes: 4 additions & 2 deletions src/language/texts/en.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { NestedTexts } from 'src/language/languages';

export function en() {
return {
altinn: {
Expand Down Expand Up @@ -28,7 +30,7 @@ export function en() {
confirm: {
answers: 'Your responses',
attachments: 'Attachments',
body: 'You are ready to submit {0}. Before you submit, we recomment that you look over and verify your responses. You cannot change your responses after submitting.',
body: 'You are ready to submit {0}. Before you submit, we recommend that you look over and verify your responses. You cannot change your responses after submitting.',
button_text: 'Submit',
deadline: 'Deadline',
sender: 'Party',
Expand Down Expand Up @@ -378,5 +380,5 @@ export function en() {
likert: {
left_column_default_header_text: 'Question',
},
};
} satisfies NestedTexts;
}
4 changes: 2 additions & 2 deletions src/language/texts/nb.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FixedLanguageList } from 'src/language/languages';
import type { FixedLanguageList, NestedTexts } from 'src/language/languages';

export function nb(): FixedLanguageList {
return {
Expand Down Expand Up @@ -381,5 +381,5 @@ export function nb(): FixedLanguageList {
likert: {
left_column_default_header_text: 'Spørsmål',
},
};
} satisfies NestedTexts;
}
4 changes: 2 additions & 2 deletions src/language/texts/nn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FixedLanguageList } from 'src/language/languages';
import type { FixedLanguageList, NestedTexts } from 'src/language/languages';

export function nn(): FixedLanguageList {
return {
Expand Down Expand Up @@ -381,5 +381,5 @@ export function nn(): FixedLanguageList {
likert: {
left_column_default_header_text: 'Spørsmål',
},
};
} satisfies NestedTexts;
}
1 change: 1 addition & 0 deletions src/layout/Address/AddressComponent.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.addressComponentPostplaceZipCode {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: space-between;
align-items: baseline;
gap: 0.937rem;
Expand Down
2 changes: 1 addition & 1 deletion src/types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export type ILanguage =
[key: string]: string | ILanguage;
};

// Language for the rendered alltinn app
// Language for the rendered altinn app
export interface IAppLanguage {
language: string; // Language code
}
Expand Down
1 change: 1 addition & 0 deletions test/e2e/integration/frontend-test/on-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe('On Entry', () => {
cy.findByRole('combobox', { name: 'Språk' }).click();
cy.findByRole('option', { name: 'Engelsk' }).click();
cy.get(appFrontend.selectInstance.header).should('contain.text', 'You have already started filling out this form');
cy.get(appFrontend.header).should('contain.text', `${appFrontend.apps.frontendTest} ENGLISH`);

cy.get('[data-testid="presentation"]').should('have.attr', 'data-expanded', 'false');
cy.findByRole('button', { name: 'Expand form' }).click();
Expand Down

0 comments on commit 7489f30

Please sign in to comment.