diff --git a/apps/explorer/src/app/app.tsx b/apps/explorer/src/app/app.tsx index 5ad134417..17d6260cb 100644 --- a/apps/explorer/src/app/app.tsx +++ b/apps/explorer/src/app/app.tsx @@ -1,3 +1,4 @@ +import '../i18n'; import { NetworkLoader, NodeFailure, @@ -28,20 +29,24 @@ function App() { ); return ( - - {t('Loading')}} - failure={} - > - - - - - - + + + {t('Loading')}} + failure={ + + } + > + + + + + + + ); } diff --git a/apps/explorer/src/app/setup-tests.ts b/apps/explorer/src/app/setup-tests.ts index 25ebbd3be..30696b54e 100644 --- a/apps/explorer/src/app/setup-tests.ts +++ b/apps/explorer/src/app/setup-tests.ts @@ -3,6 +3,9 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +import { locales } from '@vegaprotocol/i18n'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; Object.defineProperty(window, 'ResizeObserver', { writable: false, @@ -13,3 +16,14 @@ Object.defineProperty(window, 'ResizeObserver', { disconnect: jest.fn(), })), }); + +// Set up i18n instance so that components have the correct default +// en translations +i18n.use(initReactI18next).init({ + // we init with resources + resources: locales, + fallbackLng: 'en', + nsSeparator: false, + ns: ['explorer'], + defaultNS: 'explorer', +}); diff --git a/apps/explorer/src/assets/locales b/apps/explorer/src/assets/locales new file mode 120000 index 000000000..5f39c8875 --- /dev/null +++ b/apps/explorer/src/assets/locales @@ -0,0 +1 @@ +../../../../libs/i18n/src/locales \ No newline at end of file diff --git a/apps/explorer/src/i18n/index.ts b/apps/explorer/src/i18n/index.ts new file mode 100644 index 000000000..7b52db9d1 --- /dev/null +++ b/apps/explorer/src/i18n/index.ts @@ -0,0 +1,45 @@ +import type { Module } from 'i18next'; +import i18n from 'i18next'; +import HttpBackend from 'i18next-http-backend'; +import LocizeBackend from 'i18next-locize-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +const isInDev = process.env.NODE_ENV === 'development'; +const useLocize = isInDev && !!process.env.NX_USE_LOCIZE; + +const backend = useLocize + ? { + projectId: '96ac1231-4bdd-455a-b9d7-f5322a2e7430', + apiKey: process.env.NX_LOCIZE_API_KEY, + referenceLng: 'en', + } + : { + loadPath: '/assets/locales/{{lng}}/{{ns}}.json', + }; + +const Backend: Module = useLocize ? LocizeBackend : HttpBackend; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + lng: 'en', + fallbackLng: 'en', + supportedLngs: ['en'], + load: 'languageOnly', + debug: isInDev, + // have a common namespace used around the full app + ns: ['explorer'], + defaultNS: 'explorer', + keySeparator: false, // we use content as keys + nsSeparator: false, + backend, + saveMissing: useLocize && !!process.env.NX_LOCIZE_API_KEY, + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/libs/i18n/src/index.ts b/libs/i18n/src/index.ts index 1fc1495ed..947435a09 100644 --- a/libs/i18n/src/index.ts +++ b/libs/i18n/src/index.ts @@ -15,6 +15,7 @@ import en_markets from './locales/en/markets.json'; import en_web3 from './locales/en/web3.json'; import en_positions from './locales/en/positions.json'; import en_trades from './locales/en/trading.json'; +import en_ui_toolkit from './locales/en/ui-toolkit.json'; export const locales = { en: { @@ -33,5 +34,6 @@ export const locales = { web3: en_web3, positions: en_positions, trades: en_trades, + 'ui-toolkit': en_ui_toolkit, }, }; diff --git a/libs/i18n/src/locales/en/explorer.json b/libs/i18n/src/locales/en/explorer.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/libs/i18n/src/locales/en/explorer.json @@ -0,0 +1 @@ +{} diff --git a/libs/i18n/src/locales/en/ui-toolkit.json b/libs/i18n/src/locales/en/ui-toolkit.json new file mode 100644 index 000000000..763fc6002 --- /dev/null +++ b/libs/i18n/src/locales/en/ui-toolkit.json @@ -0,0 +1,21 @@ +{ + "{{fee}} Fee": "{{fee}} Fee", + "Auction Trigger stake {{trigger}}": "Auction Trigger stake {{trigger}}", + "Collapse": "Collapse", + "Copied": "Copied", + "Dark mode": "Dark mode", + "Dismiss all toasts": "Dismiss all toasts", + "Dismiss all": "Dismiss all", + "Exit view as": "Exit view as", + "Expand": "Expand", + "Light mode": "Light mode", + "Loading...": "Loading...", + "No data": "No data", + "Providers greater than 2x target stake not shown": "Providers greater than 2x target stake not shown", + "Show more": "Show more", + "Something went wrong: {{errorMessage}}": "Something went wrong: {{errorMessage}}", + "Target stake {{target}}": "Target stake {{target}}", + "This is an example of a toast notification": "This is an example of a toast notification", + "Try again": "Try again", + "Viewing as Vega user: {{pubKey}}": "Viewing as Vega user: {{pubKey}}" +} diff --git a/libs/ui-toolkit/__mocks__/react-i18next.ts b/libs/ui-toolkit/__mocks__/react-i18next.ts new file mode 100644 index 000000000..7c2343f52 --- /dev/null +++ b/libs/ui-toolkit/__mocks__/react-i18next.ts @@ -0,0 +1,14 @@ +export const useTranslation = () => ({ + t: (label: string, replacements?: Record) => { + let translatedLabel = label; + if (typeof replacements === 'object' && replacements !== null) { + Object.keys(replacements).forEach((key) => { + translatedLabel = translatedLabel.replace( + `{{${key}}}`, + replacements[key] + ); + }); + } + return translatedLabel; + }, +}); diff --git a/libs/ui-toolkit/src/components/async-renderer/async-renderer.tsx b/libs/ui-toolkit/src/components/async-renderer/async-renderer.tsx index 7ee333d7d..b74e5ccb4 100644 --- a/libs/ui-toolkit/src/components/async-renderer/async-renderer.tsx +++ b/libs/ui-toolkit/src/components/async-renderer/async-renderer.tsx @@ -1,7 +1,7 @@ import { Splash } from '../splash'; import type { ReactNode } from 'react'; -import { t } from '@vegaprotocol/i18n'; import { Button } from '../button'; +import { useT } from '../../use-t'; interface AsyncRendererProps { loading: boolean; @@ -28,15 +28,18 @@ export function AsyncRenderer({ render, reload, }: AsyncRendererProps) { + const t = useT(); if (error) { if (!data || (Array.isArray(data) && !data.length)) { return ( -
-
+
+
{errorMessage ? errorMessage - : t(`Something went wrong: ${error.message}`)} + : t('Something went wrong: {{errorMessage}}', { + errorMessage: error.message, + })} {reload && error.message === 'Timeout exceeded' && (