Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
"browser": true,
"commonjs": true
},
"globals": {
"expect": true
},
"rules": {
"no-restricted-syntax": [
"error",
Expand All @@ -26,9 +23,19 @@
},
"overrides": [
{
"files": "spec/javascripts/**/*",
"files": ["*.spec.*", "*-spec.*", "*_spec.*", "spec/**"],
"globals": {
"expect": true
},
"rules": {
"react/jsx-props-no-spreading": "off"
"react/jsx-props-no-spreading": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true,
"packageDir": "."
}
]
}
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext, useMemo, useEffect, useRef, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import AcuantContext from '../context/acuant';
import useAsset from '../hooks/use-asset';
import useI18n from '../hooks/use-i18n';
import useInstanceId from '../hooks/use-instance-id';
import useImmutableCallback from '../hooks/use-immutable-callback';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
useEffect,
useImperativeHandle,
} from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import AnalyticsContext from '../context/analytics';
import AcuantContext from '../context/acuant';
import AcuantCaptureCanvas from './acuant-capture-canvas';
import FileInput from './file-input';
import FullScreen from './full-screen';
import Button from './button';
import useI18n from '../hooks/use-i18n';
import DeviceContext from '../context/device';
import UploadContext from '../context/upload';
import useIfStillMounted from '../hooks/use-if-still-mounted';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import useI18n from '../hooks/use-i18n';
import { useI18n } from '@18f/identity-react-i18n';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import DeviceContext from '../context/device';
import useI18n from '../hooks/use-i18n';

/**
* Renders a document usage disclosure for desktop devices only. On mobile devices, an equivalent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useMemo, useContext } from 'react';
import { Alert } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import FormSteps from './form-steps';
import { UploadFormEntriesError } from '../services/upload';
import DocumentsStep, { documentsStepValidator } from './documents-step';
Expand All @@ -9,7 +10,6 @@ import ServiceProviderContext from '../context/service-provider';
import Submission from './submission';
import SubmissionStatus from './submission-status';
import DesktopDocumentDisclosure from './desktop-document-disclosure';
import useI18n from '../hooks/use-i18n';
import { RetrySubmissionError } from './submission-complete';
import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload';
import SuspenseErrorBoundary from './suspense-error-boundary';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useI18n } from '@18f/identity-react-i18n';
import AcuantCapture from './acuant-capture';
import FormErrorMessage, { CameraAccessDeclinedError } from './form-error-message';
import useI18n from '../hooks/use-i18n';

/** @typedef {import('./form-steps').FormStepError<*>} FormStepError */
/** @typedef {import('./form-steps').RegisterFieldCallback} RegisterFieldCallback */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import BlockLink from './block-link';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import useI18n from '../hooks/use-i18n';
import DeviceContext from '../context/device';
import ServiceProviderContext from '../context/service-provider';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
useRef,
useImperativeHandle,
} from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import FileImage from './file-image';
import DeviceContext from '../context/device';
import useInstanceId from '../hooks/use-instance-id';
import useI18n from '../hooks/use-i18n';
import usePrevious from '../hooks/use-previous';

/** @typedef {import('react').MouseEvent} ReactMouseEvent */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useI18n } from '@18f/identity-react-i18n';
import { UploadFormEntryError } from '../services/upload';
import { BackgroundEncryptedUploadError } from '../higher-order/with-background-encrypted-upload';
import useI18n from '../hooks/use-i18n';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { Alert } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import Button from './button';
import PageHeading from './page-heading';
import FormErrorMessage, { RequiredValueMissingError } from './form-error-message';
import PromptOnNavigate from './prompt-on-navigate';
import useI18n from '../hooks/use-i18n';
import useHistoryParam from '../hooks/use-history-param';
import useForceRender from '../hooks/use-force-render';
import useDidUpdateEffect from '../hooks/use-did-update-effect';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import useI18n from '../hooks/use-i18n';
import { useI18n } from '@18f/identity-react-i18n';
import useAsset from '../hooks/use-asset';
import useToggleBodyClassByPresence from '../hooks/use-toggle-body-class-by-presence';
import useImmutableCallback from '../hooks/use-immutable-callback';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';
import { hasMediaAccess } from '@18f/identity-device';
import useI18n from '../hooks/use-i18n';
import { useI18n } from '@18f/identity-react-i18n';
import DeviceContext from '../context/device';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import AcuantCapture from './acuant-capture';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
useImperativeHandle,
} from 'react';
import { Icon } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import FileImage from './file-image';
import useIfStillMounted from '../hooks/use-if-still-mounted';
import useI18n from '../hooks/use-i18n';
import useInstanceId from '../hooks/use-instance-id';
import useFocusFallbackRef from '../hooks/use-focus-fallback-ref';
import './selfie-capture.scss';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from 'react';
import { hasMediaAccess } from '@18f/identity-device';
import useI18n from '../hooks/use-i18n';
import { useI18n } from '@18f/identity-react-i18n';
import DeviceContext from '../context/device';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef, useEffect } from 'react';
import useI18n from '../hooks/use-i18n';
import { useI18n } from '@18f/identity-react-i18n';
import useAsset from '../hooks/use-asset';
import PageHeading from './page-heading';

Expand Down
7 changes: 0 additions & 7 deletions app/javascript/packages/document-capture/context/i18n.js

This file was deleted.

1 change: 0 additions & 1 deletion app/javascript/packages/document-capture/context/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { default as AssetContext } from './asset';
export { default as I18nContext } from './i18n';
export { default as DeviceContext } from './device';
export { default as AcuantContext, Provider as AcuantContextProvider } from './acuant';
export { default as UploadContext, Provider as UploadContextProvider } from './upload';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { createElement, cloneElement, useContext, useMemo } from 'react';
import { createElement, cloneElement, createContext, useContext, useMemo } from 'react';
import { I18n } from '@18f/identity-i18n';
import I18nContext from '../context/i18n';

/** @typedef {import('react').FC|import('react').ComponentClass} Component */

export const I18nContext = createContext({});

I18nContext.displayName = 'I18nContext';

/**
* Given an HTML string and an object of tag names to React component, returns a new React node
* where the mapped tag names are replaced by the resulting element of the rendered component.
Expand Down Expand Up @@ -49,14 +52,9 @@ export function formatHTML(html, handlers) {
return parts.filter(Boolean);
}

function useI18n() {
export function useI18n() {
const strings = useContext(I18nContext);
const { t } = useMemo(() => new I18n({ strings }), [strings]);

return {
t,
formatHTML,
};
return { t, formatHTML };
}

export default useI18n;
110 changes: 110 additions & 0 deletions app/javascript/packages/react-i18n/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useContext } from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { I18nContext, useI18n } from './index.js';

describe('I18nContext', () => {
it('defaults to empty object', () => {
const { result } = renderHook(() => useContext(I18nContext));

expect(result.current).to.deep.equal({});
});
});

describe('useI18n', () => {
describe('formatHTML', () => {
let formatHTML;
before(() => {
const { result } = renderHook(() => useI18n());
formatHTML = result.current.formatHTML;
});

it('returns html string treated as escaped text without handler', () => {
const formatted = formatHTML('Hello <strong>world</strong>!', {});

const { container } = render(formatted);

expect(container.innerHTML).to.equal('Hello &lt;strong&gt;world&lt;/strong&gt;!');
});

it('returns html string chunked by component handlers', () => {
const formatted = formatHTML('Hello <strong>world</strong>!', {
strong: ({ children }) => <strong>{children}</strong>,
});

const { container } = render(formatted);

expect(container.innerHTML).to.equal('Hello <strong>world</strong>!');
});

it('returns html string chunked by string handlers', () => {
const formatted = formatHTML('Hello <strong>world</strong>!', {
strong: 'strong',
});

const { container } = render(formatted);

expect(container.innerHTML).to.equal('Hello <strong>world</strong>!');
});

it('returns html string chunked by multiple handlers', () => {
const formatted = formatHTML(
'Message: <lg-custom>Hello</lg-custom> <strong>world</strong>!',
{
'lg-custom': () => 'Greetings',
strong: ({ children }) => <strong>{children}</strong>,
},
);

const { container } = render(formatted);

expect(container.innerHTML).to.equal('Message: Greetings <strong>world</strong>!');
});

it('removes dangling empty text fragment', () => {
const formatted = formatHTML('Hello <strong>world</strong>', {
strong: ({ children }) => <strong>{children}</strong>,
});

const { container } = render(formatted);

expect(container.childNodes).to.have.lengthOf(2);
});

it('allows (but discards) attributes in the input string', () => {
const formatted = formatHTML(
'<strong data-before>Hello</strong> <strong data-before>world</strong>',
{
strong: ({ children }) => <strong data-after>{children}</strong>,
},
);

const { container } = render(formatted);

expect(container.querySelectorAll('[data-after]')).to.have.lengthOf(2);
expect(container.querySelectorAll('[data-before]')).to.have.lengthOf(0);
});
});

describe('t', () => {
it('returns localized key value', () => {
const { result } = renderHook(() => useI18n(), {
wrapper: ({ children }) => (
<I18nContext.Provider value={{ sample: 'translation' }}>{children}</I18nContext.Provider>
),
});

const { t } = result.current;

expect(t('sample')).to.equal('translation');
});

it('falls back to key value', () => {
const { result } = renderHook(() => useI18n());

const { t } = result.current;

expect(t('sample')).to.equal('sample');
});
});
});
8 changes: 8 additions & 0 deletions app/javascript/packages/react-i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@18f/identity-react-i18n",
"private": true,
"version": "1.0.0",
"dependencies": {
"react": "^17.0.1"
}
}
2 changes: 1 addition & 1 deletion app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { render } from 'react-dom';
import {
DocumentCapture,
AssetContext,
I18nContext,
DeviceContext,
AcuantContextProvider,
UploadContextProvider,
Expand All @@ -12,6 +11,7 @@ import {
import { loadPolyfills } from '@18f/identity-polyfill';
import { isCameraCapableMobile } from '@18f/identity-device';
import { trackEvent } from '@18f/identity-analytics';
import { I18nContext } from '@18f/identity-react-i18n';

/** @typedef {import('@18f/identity-i18n').I18n} I18n */

Expand Down
1 change: 1 addition & 0 deletions config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ search:
- app/views/shared/newrelic/_browser_instrumentation.html.erb
- app/javascript/app/local-time.js
- app/javascript/packages/rails-i18n-webpack-plugin
- app/javascript/packages/react-i18n
- app/javascript/packages/i18n

## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"@testing-library/react": "^11.2.2",
"@testing-library/react-hooks": "^3.7.0",
"@testing-library/user-event": "^12.6.0",
"@types/chai": "^4.2.22",
"@types/dirty-chai": "^2.0.2",
"@types/mocha": "^9.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/sinon": "^9.0.11",
Expand All @@ -61,6 +64,7 @@
"sinon-chai": "^3.5.0",
"svgo": "^1.3.2",
"typescript": "^4.1.3",
"webpack": "^4.46.0",
"webpack-dev-server": "^3.11.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AcuantCapture, {
} from '@18f/identity-document-capture/components/acuant-capture';
import { AcuantContextProvider, AnalyticsContext } from '@18f/identity-document-capture';
import DeviceContext from '@18f/identity-document-capture/context/device';
import I18nContext from '@18f/identity-document-capture/context/i18n';
import { I18nContext } from '@18f/identity-react-i18n';
import { render, useAcuant } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';

Expand Down
Loading