Compare commits

...

14 Commits

Author SHA1 Message Date
Gabe Kangas 3f3a602f57 New translations strings.json (German) 2 years ago
Gabe Kangas fae7e0e88c Run translation job when web components are updated 2 years ago
Gabe Kangas fb72be0b6b Tooltip i18n 2 years ago
Gabe Kangas 0a05ee87cf Point to updated translated files 2 years ago
github-actions[bot] bfdd6086d6
New Crowdin translations by GitHub Action (#3448) 2 years ago
Gabe Kangas 3444a0f7b1 Update configs 2 years ago
Gabe Kangas e4edbd2735 Update crowdlin config 2 years ago
Gabe Kangas 9c9333a07d try to fix the multiple parsing of a file 2 years ago
Gabe Kangas 5f8aca8d51 Update defaults again 2 years ago
Gabe Kangas 8fdc8b1e79 Update parser config 2 years ago
Gabe Kangas 3d741015fd Update default value 2 years ago
Gabe Kangas cf1f46fa5e Update CI job 2 years ago
Gabe Kangas 8497c3f60b Add CI job for translations 2 years ago
Gabe Kangas 1c8b9d0728 First pass at configuring localization 2 years ago
  1. 55
      .github/workflows/translations.yml
  2. 10
      crowdin.yml
  3. 27
      web/components/action-buttons/NotifyButton.tsx
  4. 10
      web/components/ui/Footer/Footer.tsx
  5. 14
      web/components/ui/Header/Header.tsx
  6. 6
      web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx
  7. 21
      web/components/ui/OfflineBanner/OfflineBanner.tsx
  8. 19
      web/i18n/de.json
  9. 19
      web/i18n/en.json
  10. 19
      web/i18n/es.json
  11. 19
      web/i18n/fr.json
  12. 19
      web/i18n/index.js
  13. 19
      web/i18n/strings.json
  14. 113
      web/i18next-parser.config.mjs
  15. 1424
      web/package-lock.json
  16. 8
      web/package.json
  17. 2
      web/pages/_document.tsx

55
.github/workflows/translations.yml

@ -0,0 +1,55 @@
name: Translation job
on:
push:
paths:
- 'web/i18n/strings.json'
- 'web/**/*.tsx'
- 'web/**/*.js'
- 'crowdin.yml'
- '.github/workflows/translations.yml'
- 'web/i18next-parser.config.mjs'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
generate-translations:
defaults:
run:
working-directory: ./web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
run: npm install
- name: Generate translation files
run: npm run translate
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Commit updated translations'
add: 'web/i18n/**'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Crowdin action
uses: crowdin/github-action@v1
with:
upload_sources: true
download_translations: true
localization_branch_name: translations
config: crowdin.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

10
crowdin.yml

@ -0,0 +1,10 @@
'pull_request_title': 'Translations update'
'pull_request_labels': ['crowdin', 'i18n']
'commit_message': 'Updated translations'
files:
- source: /web/i18n/strings.json
translation: /web/i18n/%two_letters_code%.json
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN

27
web/components/action-buttons/NotifyButton.tsx

@ -1,6 +1,7 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { FC } from 'react'; import { FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import styles from './ActionButton/ActionButton.module.scss'; import styles from './ActionButton/ActionButton.module.scss';
// Lazy loaded components // Lazy loaded components
@ -14,14 +15,18 @@ export type NotifyButtonProps = {
onClick?: () => void; onClick?: () => void;
}; };
export const NotifyButton: FC<NotifyButtonProps> = ({ onClick, text }) => ( export const NotifyButton: FC<NotifyButtonProps> = ({ onClick, text }) => {
<Button const { t } = useTranslation();
type="primary"
className={styles.button} return (
icon={<BellFilled />} <Button
onClick={onClick} type="primary"
id="notify-button" className={styles.button}
> icon={<BellFilled />}
{text || 'Notify'} onClick={onClick}
</Button> id="notify-button"
); >
{text || t('Notify')}
</Button>
);
};

10
web/components/ui/Footer/Footer.tsx

@ -1,5 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useTranslation } from 'next-export-i18n';
import styles from './Footer.module.scss'; import styles from './Footer.module.scss';
import { ServerStatus } from '../../../interfaces/server-status.model'; import { ServerStatus } from '../../../interfaces/server-status.model';
import { serverStatusState } from '../../stores/ClientConfigStore'; import { serverStatusState } from '../../stores/ClientConfigStore';
@ -7,20 +8,21 @@ import { serverStatusState } from '../../stores/ClientConfigStore';
export const Footer: FC = () => { export const Footer: FC = () => {
const clientStatus = useRecoilValue<ServerStatus>(serverStatusState); const clientStatus = useRecoilValue<ServerStatus>(serverStatusState);
const { versionNumber } = clientStatus; const { versionNumber } = clientStatus;
const { t } = useTranslation();
return ( return (
<footer className={styles.footer} id="footer"> <footer className={styles.footer} id="footer">
<span> <span>
Powered by <a href="https://owncast.online">Owncast v{versionNumber}</a> {t('Powered by Owncast')} <a href="https://owncast.online">v{versionNumber}</a>
</span> </span>
<span className={styles.links}> <span className={styles.links}>
<a href="https://owncast.online/docs" target="_blank" rel="noreferrer"> <a href="https://owncast.online/docs" target="_blank" rel="noreferrer">
Documentation {t('Documentation')}
</a> </a>
<a href="https://owncast.online/help" target="_blank" rel="noreferrer"> <a href="https://owncast.online/help" target="_blank" rel="noreferrer">
Contribute {t('Contribute')}
</a> </a>
<a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer"> <a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer">
Source {t('Source')}
</a> </a>
</span> </span>
</footer> </footer>

14
web/components/ui/Header/Header.tsx

@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react';
import cn from 'classnames'; import cn from 'classnames';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslation } from 'next-export-i18n';
import styles from './Header.module.scss'; import styles from './Header.module.scss';
// Lazy loaded components // Lazy loaded components
@ -23,6 +24,7 @@ export type HeaderComponentProps = {
export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => { export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => {
const [canHideChat, setCanHideChat] = useState(false); const [canHideChat, setCanHideChat] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setCanHideChat(window.innerWidth >= 768); setCanHideChat(window.innerWidth >= 768);
@ -32,18 +34,18 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
<header className={cn([`${styles.header}`], 'global-header')}> <header className={cn([`${styles.header}`], 'global-header')}>
{online ? ( {online ? (
<Link href="#player" className={styles.skipLink}> <Link href="#player" className={styles.skipLink}>
Skip to player {t('Skip to player')}
</Link> </Link>
) : ( ) : (
<Link href="#offline-message" className={styles.skipLink}> <Link href="#offline-message" className={styles.skipLink}>
Skip to offline message {t('Skip to offline message')}
</Link> </Link>
)} )}
<Link href="#skip-to-content" className={styles.skipLink}> <Link href="#skip-to-content" className={styles.skipLink}>
Skip to page content {t('Skip to page content')}
</Link> </Link>
<Link href="#footer" className={styles.skipLink}> <Link href="#footer" className={styles.skipLink}>
Skip to footer {t('Skip to footer')}
</Link> </Link>
<div className={styles.logo}> <div className={styles.logo}>
<div id="header-logo" className={styles.logoImage}> <div id="header-logo" className={styles.logoImage}>
@ -59,10 +61,10 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
{!chatAvailable && !chatDisabled && ( {!chatAvailable && !chatDisabled && (
<Tooltip <Tooltip
overlayClassName={styles.toolTip} overlayClassName={styles.toolTip}
title="Chat will be available when the stream is live." title={t('Chat will be available when the stream is live.')}
placement="left" placement="left"
> >
<span className={styles.chatOfflineText}>Chat is offline</span> <span className={styles.chatOfflineText}>{t('Chat is offline')}</span>
</Tooltip> </Tooltip>
)} )}
</header> </header>

6
web/components/ui/NotifyReminderPopup/NotifyReminderPopup.tsx

@ -1,5 +1,6 @@
import React, { useState, useEffect, FC } from 'react'; import React, { useState, useEffect, FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import styles from './NotifyReminderPopup.module.scss'; import styles from './NotifyReminderPopup.module.scss';
import { Popover } from '../Popover/Popover'; import { Popover } from '../Popover/Popover';
@ -24,6 +25,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
}) => { }) => {
const [openPopup, setOpenPopup] = useState(open); const [openPopup, setOpenPopup] = useState(open);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setOpenPopup(open); setOpenPopup(open);
@ -33,7 +35,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
setMounted(true); setMounted(true);
}, []); }, []);
const title = <div className={styles.title}>Stay updated!</div>; const title = <div className={styles.title}>{t('Stay updated!')}</div>;
const popupClicked = e => { const popupClicked = e => {
e.stopPropagation(); e.stopPropagation();
@ -56,7 +58,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
> >
<CloseOutlined /> <CloseOutlined />
</button> </button>
<div className={styles.contentbutton}>Click and never miss future streams!</div> <div className={styles.contentbutton}>{t('Click and never miss future streams!')}</div>
</div> </div>
); );

21
web/components/ui/OfflineBanner/OfflineBanner.tsx

@ -5,6 +5,7 @@ import { FC } from 'react';
import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'next-export-i18n';
import styles from './OfflineBanner.module.scss'; import styles from './OfflineBanner.module.scss';
// Lazy loaded components // Lazy loaded components
@ -36,13 +37,15 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
onFollowClick, onFollowClick,
className, className,
}) => { }) => {
const { t } = useTranslation();
let text; let text;
if (customText) { if (customText) {
text = customText; text = customText;
} else if (!customText && notificationsEnabled && fediverseAccount) { } else if (!customText && notificationsEnabled && fediverseAccount) {
text = ( text = (
<span> <span>
This stream is offline. You can{' '} {t('This stream is offline. You can')}{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}> <span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}>
be notified be notified
</span>{' '} </span>{' '}
@ -56,21 +59,25 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
} else if (!customText && notificationsEnabled) { } else if (!customText && notificationsEnabled) {
text = ( text = (
<span> <span>
This stream is offline.{' '} {t('This stream is offline')}.{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}> <span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}>
Be notified Be notified
</span>{' '} </span>{' '}
the next time {streamName} goes live. {t('the next time goes live', { streamer: streamName })}.
</span> </span>
); );
} else if (!customText && fediverseAccount) { } else if (!customText && fediverseAccount) {
text = ( text = (
<span> <span>
This stream is offline.{' '} {t('This stream is offline.')}{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onFollowClick}> <span role="link" tabIndex={0} className={styles.actionLink} onClick={onFollowClick}>
Follow {t('Follow')}
</span>{' '} </span>{' '}
{fediverseAccount} on the Fediverse to see the next time {streamName} goes live. {t('on the Fediverse to see the next time goes live', {
fediverseAccount,
streamer: streamName,
})}
.
</span> </span>
); );
} else { } else {
@ -95,7 +102,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
{lastLive && ( {lastLive && (
<div className={styles.lastLiveDate}> <div className={styles.lastLiveDate}>
<ClockCircleOutlined className={styles.clockIcon} /> <ClockCircleOutlined className={styles.clockIcon} />
{`Last live ${formatDistanceToNow(new Date(lastLive))} ago.`} {`${(t('Last live ago'), { timeAgo: formatDistanceToNow(new Date(lastLive)) })}`}
</div> </div>
)} )}
</div> </div>

19
web/i18n/de.json

@ -0,0 +1,19 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago"
}

19
web/i18n/en.json

@ -0,0 +1,19 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago"
}

19
web/i18n/es.json

@ -0,0 +1,19 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago"
}

19
web/i18n/fr.json

@ -0,0 +1,19 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago"
}

19
web/i18n/index.js

@ -0,0 +1,19 @@
const en = require('./en.json');
const es = require('./es.json');
const de = require('./de.json');
const fr = require('./fr.json');
const i18n = {
translations: {
en,
es,
de,
fr,
},
defaultLang: 'en',
useBrowserDefault: true,
// optional property, will default to "query" if not set
languageDataStore: 'query' || 'localStorage',
};
module.exports = i18n;

19
web/i18n/strings.json

@ -0,0 +1,19 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago"
}

113
web/i18next-parser.config.mjs

@ -0,0 +1,113 @@
// i18next-parser.config.js
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: true,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: function (locale, namespace, key, value) {
return `${key}`;
}, // Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: false,
// Keep keys from the catalog that are no longer in code
// You may either specify a boolean to keep or discard all removed keys.
// You may also specify an array of patterns: the keys from the catalog that are no long in the code but match one of the patterns will be kept.
// The patterns are applied to the full key including the namespace, the parent keys and the separators.
keySeparator: '.',
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
hbs: ['HandlebarsLexer'],
handlebars: ['HandlebarsLexer'],
htm: ['HTMLLexer'],
html: ['HTMLLexer'],
mjs: ['JavascriptLexer'],
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en'],
// An array of the locales in your applications
namespaceSeparator: ':',
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: 'i18n/strings.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: '_',
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
// If you don't want to generate keys for plurals (for example, in case you are using ICU format), set `pluralSeparator: false`.
input: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: false,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
// i18nextOptions: { returnDetails: true, lng: '$LOCALE' },
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
};

1424
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

8
web/package.json

@ -11,7 +11,8 @@
"build-storybook": "storybook build", "build-storybook": "storybook build",
"build-styles": "cd ./style-definitions && style-dictionary build && ./build.sh && cd -", "build-styles": "cd ./style-definitions && style-dictionary build && ./build.sh && cd -",
"test": "jest", "test": "jest",
"format": "prettier --write **/*.{js,ts,jsx,tsx,css,md}" "format": "prettier --write **/*.{js,ts,jsx,tsx,css,md}",
"translate": "i18next -c i18next-parser.config.mjs"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "4.8.1", "@ant-design/icons": "4.8.1",
@ -32,10 +33,12 @@
"classnames": "2.3.2", "classnames": "2.3.2",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"i18next-parser": "^8.9.0",
"interweave": "^13.0.0", "interweave": "^13.0.0",
"interweave-autolink": "^5.1.0", "interweave-autolink": "^5.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"next": "14.0.1", "next": "14.0.1",
"next-export-i18n": "^2.1.0",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"next-with-less": "3.0.1", "next-with-less": "3.0.1",
"picmo": "5.8.5", "picmo": "5.8.5",
@ -107,6 +110,7 @@
"eslint-plugin-storybook": "0.6.15", "eslint-plugin-storybook": "0.6.15",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html-webpack-plugin": "5.5.3", "html-webpack-plugin": "5.5.3",
"i18next-scanner": "^4.4.0",
"install": "^0.13.0", "install": "^0.13.0",
"knip": "^2.11.0", "knip": "^2.11.0",
"less": "4.2.0", "less": "4.2.0",
@ -130,4 +134,4 @@
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "5.3.2" "typescript": "5.3.2"
} }
} }

2
web/pages/_document.tsx

@ -24,7 +24,7 @@ class InlineStylesHead extends Head {
export default function Document() { export default function Document() {
return ( return (
<Html lang="en"> <Html>
<InlineStylesHead /> <InlineStylesHead />
<body> <body>
<Main /> <Main />

Loading…
Cancel
Save