First pass at configuring localization

This commit is contained in:
Gabe Kangas 2023-11-27 21:33:23 -08:00
parent 509774e5ae
commit 1c8b9d0728
16 changed files with 1682 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

3
web/crowdin.yml Normal file
View File

@ -0,0 +1,3 @@
files:
- source: /translations/locales/en-us.po
translation: /translations/locales/%locale%.po

19
web/i18n/index.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,113 @@
// i18next-parser.config.js
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: false,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: function (locale, namespace, key, value) {
return `Needs \"${locale}\" translation: ${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', 'fr', 'es', 'sp', 'de'],
// 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/translations.$LOCALE.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: true,
// 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: null,
// 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

View File

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

View File

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