Compare commits

...

14 Commits

Author SHA1 Message Date
Gabe Kangas 3f3a602f57 New translations strings.json (German) 1 year ago
Gabe Kangas fae7e0e88c Run translation job when web components are updated 1 year ago
Gabe Kangas fb72be0b6b Tooltip i18n 1 year ago
Gabe Kangas 0a05ee87cf Point to updated translated files 1 year ago
github-actions[bot] bfdd6086d6
New Crowdin translations by GitHub Action (#3448) 1 year ago
Gabe Kangas 3444a0f7b1 Update configs 1 year ago
Gabe Kangas e4edbd2735 Update crowdlin config 1 year ago
Gabe Kangas 9c9333a07d try to fix the multiple parsing of a file 1 year ago
Gabe Kangas 5f8aca8d51 Update defaults again 1 year ago
Gabe Kangas 8fdc8b1e79 Update parser config 1 year ago
Gabe Kangas 3d741015fd Update default value 1 year ago
Gabe Kangas cf1f46fa5e Update CI job 1 year ago
Gabe Kangas 8497c3f60b Add CI job for translations 1 year ago
Gabe Kangas 1c8b9d0728 First pass at configuring localization 1 year 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 @@ @@ -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 @@ @@ -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 @@ @@ -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 = { @@ -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>
);
};

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

@ -1,5 +1,6 @@ @@ -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'; @@ -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>

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

@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react'; @@ -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 = { @@ -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 @@ -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}>
@ -59,10 +61,10 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa @@ -59,10 +61,10 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
{!chatAvailable && !chatDisabled && (
<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"
>
<span className={styles.chatOfflineText}>Chat is offline</span>
<span className={styles.chatOfflineText}>{t('Chat is offline')}</span>
</Tooltip>
)}
</header>

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

@ -1,5 +1,6 @@ @@ -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> = ({ @@ -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> = ({ @@ -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> = ({ @@ -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>
);

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

@ -5,6 +5,7 @@ import { FC } from 'react'; @@ -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> = ({ @@ -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> = ({ @@ -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> = ({ @@ -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>

19
web/i18n/de.json

@ -0,0 +1,19 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -130,4 +134,4 @@
"ts-jest": "^29.1.0",
"typescript": "5.3.2"
}
}
}

2
web/pages/_document.tsx

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

Loading…
Cancel
Save