diff --git a/.eslintrc.json b/.eslintrc.json index bc38f51d..4ded5dda 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -59,6 +59,13 @@ "react-hooks/exhaustive-deps": "error", "react/react-in-jsx-scope": "off", "react/jsx-uses-react": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], "@typescript-eslint/no-explicit-any": "off" }, "overrides": [ diff --git a/cspell.json b/cspell.json index b497a076..503ba166 100644 --- a/cspell.json +++ b/cspell.json @@ -17,6 +17,7 @@ "temp", "*.svg", "pnpm-lock.yaml", + ".eslintrc.json", "cspell-dictionary.txt" ] } diff --git a/src/background/container.ts b/src/background/container.ts index 2d303618..58c36938 100644 --- a/src/background/container.ts +++ b/src/background/container.ts @@ -13,6 +13,7 @@ import { } from './services' import { createLogger, Logger } from '@/shared/logger' import { LOG_LEVEL } from '@/shared/defines' +import { tFactory, type Translation } from '@/shared/helpers' interface Cradle { logger: Logger @@ -25,6 +26,7 @@ interface Cradle { sendToPopup: SendToPopup tabEvents: TabEvents background: Background + t: Translation tabState: TabState } @@ -38,6 +40,7 @@ export const configureContainer = () => { container.register({ logger: asValue(logger), browser: asValue(browser), + t: asValue(tFactory(browser)), events: asClass(EventsService).singleton(), deduplicator: asClass(Deduplicator) .singleton() diff --git a/src/background/services/tabEvents.ts b/src/background/services/tabEvents.ts index ddd4bb24..2feab400 100644 --- a/src/background/services/tabEvents.ts +++ b/src/background/services/tabEvents.ts @@ -4,6 +4,7 @@ import { MonetizationService } from './monetization' import { StorageService } from './storage' import { IsTabMonetizedPayload } from '@/shared/messages' import { getTabId } from '../utils' +import type { Translation } from '@/shared/helpers' const runtime = browser.runtime const ICONS = { @@ -29,6 +30,7 @@ export class TabEvents { constructor( private monetizationService: MonetizationService, private storage: StorageService, + private t: Translation, private browser: Browser ) {} clearTabSessions = ( @@ -63,14 +65,14 @@ export class TabEvents { ) => { const { enabled } = await this.storage.get(['enabled']) - let title = this.browser.i18n.getMessage('appName') + let title = this.t('appName') let iconData = enabled ? ICONS.default : ICONS.warning if (enabled && payload) { const { value: isTabMonetized } = payload iconData = isTabMonetized ? ICONS.active : ICONS.inactive const tabStateText = isTabMonetized - ? this.browser.i18n.getMessage('monetizationActiveShort') - : this.browser.i18n.getMessage('monetizationInactiveShort') + ? this.t('monetizationActiveShort') + : this.t('monetizationInactiveShort') title = `${title} - ${tabStateText}` } const tabId = sender && getTabId(sender) diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index ab7ca67c..6fd2b49f 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,7 +1,8 @@ import { MainLayout } from '@/popup/components/layout/MainLayout' -import { PopupContextProvider } from './lib/context' +import { PopupContextProvider, TranslationContextProvider } from './lib/context' import { LazyMotion, domAnimation } from 'framer-motion' import React from 'react' +import browser from 'webextension-polyfill' import { ProtectedRoute } from '@/popup/components/ProtectedRoute' import { RouteObject, @@ -49,9 +50,11 @@ const router = createMemoryRouter(routes) export const Popup = () => { return ( - - - + + + + + ) } diff --git a/src/popup/components/SiteNotMonetized.tsx b/src/popup/components/SiteNotMonetized.tsx index 813a3575..b5c7ba57 100644 --- a/src/popup/components/SiteNotMonetized.tsx +++ b/src/popup/components/SiteNotMonetized.tsx @@ -1,16 +1,15 @@ import React from 'react' -import browser from 'webextension-polyfill' import { WarningSign } from '@/popup/components/Icons' +import { useTranslation } from '@/popup/lib/context' export const SiteNotMonetized = () => { + const t = useTranslation() return (
-

- {browser.i18n.getMessage('siteNotMonetized')} -

+

{t('siteNotMonetized')}

) } diff --git a/src/popup/lib/context.tsx b/src/popup/lib/context.tsx index e656cad4..ab16064d 100644 --- a/src/popup/lib/context.tsx +++ b/src/popup/lib/context.tsx @@ -1,7 +1,8 @@ import React from 'react' -import browser from 'webextension-polyfill' +import browser, { type Browser } from 'webextension-polyfill' import { getContextData } from '@/popup/lib/messages' -import { DeepNonNullable, PopupStore } from '@/shared/types' +import { tFactory, type Translation } from '@/shared/helpers' +import type { DeepNonNullable, PopupStore } from '@/shared/types' import { ContentToBackgroundAction, type ContentToBackgroundMessage @@ -161,3 +162,23 @@ export function PopupContextProvider({ children }: PopupContextProviderProps) { ) } + +const TranslationContext = React.createContext((v: string) => v) + +export const TranslationContextProvider = ({ + browser, + children +}: { + browser: Browser + children: React.ReactNode +}) => { + const t = tFactory(browser) + + return ( + + {children} + + ) +} + +export const useTranslation = () => React.useContext(TranslationContext) diff --git a/src/popup/pages/MissingHostPermission.tsx b/src/popup/pages/MissingHostPermission.tsx index 954c37f7..c4a015eb 100644 --- a/src/popup/pages/MissingHostPermission.tsx +++ b/src/popup/pages/MissingHostPermission.tsx @@ -2,8 +2,10 @@ import React from 'react' import browser from 'webextension-polyfill' import { PERMISSION_HOSTS } from '@/shared/defines' import { WarningSign } from '@/popup/components/Icons' +import { useTranslation } from '@/popup/lib/context' export const Component = () => { + const t = useTranslation() return (
@@ -13,7 +15,7 @@ export const Component = () => {

Permission needed

-

{browser.i18n.getMessage('hostsPermissionsNeeded')}

+

{t('hostsPermissionsNeeded')}

diff --git a/src/shared/helpers.ts b/src/shared/helpers.ts index bc2d65c6..d0057ff8 100644 --- a/src/shared/helpers.ts +++ b/src/shared/helpers.ts @@ -2,6 +2,7 @@ import { SuccessResponse } from '@/shared/messages' import { WalletAddress } from '@interledger/open-payments/dist/types' import { cx, CxOptions } from 'class-variance-authority' import { twMerge } from 'tailwind-merge' +import type { Browser } from 'webextension-polyfill' export const cn = (...inputs: CxOptions) => { return twMerge(cx(inputs)) @@ -193,6 +194,20 @@ export function bigIntMax(a: string, b: string) { return BigInt(a) > BigInt(b) ? a : b } +type TranslationKeys = keyof typeof import('../_locales/en/messages.json') + +export type Translation = ReturnType +export function tFactory(browser: Pick) { + /** + * Helper over calling cumbersome `this.browser.i18n.getMessage(key)` with + * added benefit that it type-checks if key exists in message.json + */ + return ( + key: T, + substitutions?: string | string[] + ) => browser.i18n.getMessage(key, substitutions) +} + type Primitive = string | number | boolean | null | undefined // Warn: Not a nested object equals or a deepEquals function