1
0
Fork 0

feat(org): Added various translation pages

This commit is contained in:
joostdecock 2023-07-09 18:50:13 +02:00
parent 2299cc6ade
commit b822e35fbe
11 changed files with 548 additions and 17 deletions

View file

@ -19,16 +19,6 @@ We currently support the following five languages:
- **es** : Spanish
- **fr** : French
- **nl** : Dutch
## Incubator Languages
For the following languages, our community has started an effort, but that
effort has not yet reached the level of maturity that to make it a supported
language.
In other words, **these are the languages where we are most in need of extra
translators**:
- **uk** : Ukranian
## Become a FreeSewing translator
@ -53,7 +43,7 @@ Discord](https://discord.freesewing.org) for any questions that may remain.
## Adding a new language
We would love to make FreeSewing available in more langauges. If you are
We would love to make FreeSewing available in more langauges. If you are
interested in starting a new translation effort, that is great.
We ask that you familiarize yourself with this translation guide to understand
@ -62,7 +52,7 @@ a new language with the link below.
<Link compact>
###### [Request to setup a new FreeSewing language](https://next.freesewing.org/translation/add-language)
###### [Suggest a new FreeSewing language](https://next.freesewing.org/translation/suggest-language)
</Link>
<Fixme compact>

View file

@ -0,0 +1,221 @@
// Dependencies
import { siteConfig } from 'site/site.config.mjs'
import translators from 'site/prebuild/translators.json'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Components
import { ChoiceButton } from 'shared/components/choice-button.mjs'
import { I18nIcon } from 'shared/components/icons.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
export const ns = ['translation', 'locales']
/*
* Note that this is not a list of all languages.
* Instead it is a list of languages that are supported by DeepL
* and not yet available (Our Crowdin is integrated with DeepL.
*/
const languages = [
'Bulgarian',
'Chinese (simplified)',
'Czech',
'Danish',
'Estonian',
'Finnish',
'Greek',
'Hungarian',
'Indonesian',
'Italian',
'Japanese',
'Korean',
'Latvian',
'Lithuanian',
'Norwegian',
'Polish',
'Portuguese',
'Romanian',
'Russian',
'Slovak',
'Slovenian',
'Swedish',
'Turkish',
]
const TeamList = ({ language, t }) => {
const count = Object.keys(translators[language]).length
return (
<>
{Object.keys(translators[language])
.sort()
.map((name, i) => (
<span key={name}>
<b>{name}</b>
{i < count - 2 ? ', ' : i < count - 1 ? ' & ' : ' '}
</span>
))}
</>
)
}
export const SuggestLanguageForm = () => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { t } = useTranslation(ns)
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
// State
const [language, setLanguage] = useState(false)
const [sent, setSent] = useState(false)
const [help, setHelp] = useState(0)
const [friends, setFriends] = useState(0)
const [comments, setComments] = useState('')
const sendSuggestion = async () => {
startLoading()
const result = await backend.sendLanguageSuggestion({ language, help, friends, comments })
if (result.success) {
setSent(true)
stopLoading()
toast.success('Suggestion submitted')
} else {
toast.for.backendError()
}
}
if (sent)
return (
<>
<Popout note>
<h5>Suggestion submitted</h5>
<p>
We will get back to you shortly. Thank you for taking an interest in bringing FreeSewing
to more people, specifically the {language} community.
</p>
</Popout>
</>
)
return (
<>
<h5 className="mt-4">In what language would you like make FreeSewing available?</h5>
<select
className="select select-bordered w-full border-neutral"
onChange={(evt) => setLanguage(evt.target.value)}
>
<option value="">Please choose a language</option>
{languages.map((l) => (
<option value={l} key={l}>
{l}
</option>
))}
</select>
{language ? (
<>
<h5 className="mt-4">
Do you plan to (help) translate FreeSewing to this language yourself?
</h5>
<div className="flex flex-col gap-2 lg:grid lg:grid-cols-2">
<ChoiceButton noMargin title="Yes, I do" onClick={() => setHelp(1)} active={help === 1}>
<span>I am able and willing to help out with translation</span>
</ChoiceButton>
<ChoiceButton
noMargin
title="No, I do not"
onClick={() => setHelp(-1)}
active={help === -1}
>
<span>I do not intent to help out with translation</span>
</ChoiceButton>
</div>
</>
) : (
<Popout tip>
<h5>Are you looking to suggest a language that is not in the list?</h5>
<p>
The list of languages above does obviously not include <em>all</em> languages. Instead,
it is limimted to the list of langauges that are supported by{' '}
<WebLink href="https://www.deepl.com/" txt="DeepL" />, a machine-learning tool that can
help translators with suggestions that make for an efficient translation experience.
</p>
<p>
It is always possible to translate to another language by translating everything by
hand. However, we estimate that the amount of people out there who are willing to take
on such a task is a rounding error.
</p>
<p>
If you are committed to translating FreeSewing to a language not in the list above,
please <WebLink href="https://discord.freesewing.org/" txt="please readh out to us" />.
</p>
</Popout>
)}
{language && help < 0 && (
<Popout note>
<h5>Thank you for your suggestion</h5>
<p>
We appreciate that you would like to see FreeSewing translated to {language}.
<br />
However, since you are unable to make a commitment yourself, we will not process your
suggestion.
</p>
<p>
We rely on the community to provide translation. We encourage you to find people in the{' '}
{language} maker/sewing community who may want to take an active part in translating
FreeSewing into {language}.
</p>
<p>You can then send those people to this same page to suggest {language} again.</p>
</Popout>
)}
{language && help > 0 && (
<>
<h5 className="mt-4">Do you have friends who can help?</h5>
<div className="flex flex-col gap-2 lg:grid lg:grid-cols-2">
<ChoiceButton
noMargin
title="Hell yeah I do"
onClick={() => setFriends(1)}
active={friends === 1}
>
<span>And they want to help out too!</span>
</ChoiceButton>
<ChoiceButton
noMargin
title="Not really"
onClick={() => setFriends(-1)}
active={friends === -1}
>
<span>But maybe others will join my effort?</span>
</ChoiceButton>
</div>
</>
)}
{language && help > 0 && friends !== 0 && (
<>
<h5 className="mt-4">Any comments you would like to add?</h5>
<textarea
value={comments}
placeholder="Type your commnents here"
className="textarea textarea-bordered w-full border border-neutral"
onChange={(evt) => setComments(evt.target.value)}
/>
<p className="mt-8 text-center">
<button className="btn btn-primary" onClick={sendSuggestion}>
Submit Suggestion
</button>
</p>
</>
)}
</>
)
}

View file

@ -0,0 +1,126 @@
// Dependencies
import { siteConfig } from 'site/site.config.mjs'
import translators from 'site/prebuild/translators.json'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Components
import { ChoiceButton } from 'shared/components/choice-button.mjs'
import { I18nIcon } from 'shared/components/icons.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
export const ns = ['translation', 'locales']
const languages = [
...siteConfig.languages.filter((lang) => lang !== 'en'),
...siteConfig.languagesWip,
].sort()
const TeamList = ({ language, t }) => {
const count = Object.keys(translators[language]).length
return (
<>
{Object.keys(translators[language])
.sort()
.map((name, i) => (
<span key={name}>
<b>{name}</b>
{i < count - 2 ? ', ' : i < count - 1 ? ' & ' : ' '}
</span>
))}
</>
)
}
export const TranslatorInvite = () => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { t } = useTranslation(ns)
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
// State
const [team, setTeam] = useState(false)
const [sent, setSent] = useState(false)
const sendInvite = async () => {
startLoading()
const result = await backend.sendTranslatorInvite(team)
if (result.success) {
setSent(true)
stopLoading()
toast.success(t('translation:inviteSent'))
} else {
toast.for.backendError()
}
}
if (sent)
return (
<>
<Popout tip>
<h5>{t('translation:inviteSent')}</h5>
<p>{t('translation:successNote')}</p>
</Popout>
<Popout link compact>
<WebLink
href="https://freesewing.dev/guides/translation"
txt={t('translation:seeTranslationGuide')}
/>
</Popout>
</>
)
return team ? (
<>
<p>
<button className="btn btn-primary mr-2" onClick={sendInvite}>
{t(`locales:sendMeAnInvite`)}: {t(`locales:${team}`)}
</button>
<button className="btn btn-primary btn-outline" onClick={() => setTeam(false)}>
Join a different team
</button>
</p>
</>
) : (
<>
<p>{t('translation:pleaseChooseTeam')}</p>
<h5>{t('translation:whatTeam')}</h5>
<div className="flex flex-col gap-2 lg:grid lg:grid-cols-2 gap-2 mt- mb-82">
{languages.map((language) => {
const count = Object.keys(translators[language]).length
return (
<ChoiceButton
noMargin
key={language}
icon={<I18nIcon />}
title={t('translation:languageTeam', { language: t('locales:' + language) })}
onClick={() => setTeam(language)}
>
<div className="text-sm flex flex-row flex-wrap gap-1">
{Object.keys(translators[language]).map((name, i) => (
<span
key={i}
className="bg-secondary bg-opacity-10 rounded px-2 text-sm border border-secondary"
>
{name}
</span>
))}
</div>
</ChoiceButton>
)
})}
</div>
</>
)
}

View file

@ -93,6 +93,16 @@ const sitePages = (t = false, control = 99) => {
t: t('translation'),
s: 'translation',
h: 1,
join: {
t: t('translation:joinATranslationTeam'),
s: 'translation',
h: 1,
},
'suggest-language': {
t: t('translation:suggestLanguage'),
s: 'translation',
h: 1,
},
},
sitemap: {
t: t('sitemap'),

View file

@ -10,6 +10,7 @@ import { Translators } from 'site/components/crowdin/translators.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
import Link from 'next/link'
// Translation namespaces used on this page
const namespaces = [...new Set(pageNs), 'translation', 'locales']
@ -30,7 +31,13 @@ const TranslationPage = ({ page }) => {
<Popout tip>
<h5>{t('translation:getInvolved')}</h5>
<p>{t('translation:teamEffort')}</p>
<a href="https://freesewing.dev/guides/translation" className="btn btn-accent">
<Link href="/translation/join" className="btn btn-accent mr-2">
{t('translation:joinTheTeam')}
</Link>
<a
href="https://freesewing.dev/guides/translation"
className="btn btn-accent btn-outline"
>
{t('translation:seeTranslationGuide')}
</a>
</Popout>
@ -92,7 +99,13 @@ const TranslationPage = ({ page }) => {
<br />
{t('translation:addLanguage3')}
</p>
<a href="https://freesewing.dev/guides/translation" className="btn btn-accent">
<Link href="/translation/suggest-language" className="btn btn-accent mr-2">
{t('translation:suggestLanguage')}
</Link>
<a
href="https://freesewing.dev/guides/translation"
className="btn btn-accent btn-outline"
>
{t('translation:seeTranslationGuide')}
</a>
</Popout>

View file

@ -0,0 +1,70 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { BareLayout as Layout } from 'site/components/layouts/bare.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(pageNs), 'translation', 'locales']
/*
* Some things should never generated as SSR
* So for these, we run a dynamic import and disable SSR rendering
*/
const DynamicAuthWrapper = dynamic(
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
{ ssr: false }
)
const DynamicForm = dynamic(
() => import('site/components/crowdin/translator-invite.mjs').then((mod) => mod.TranslatorInvite),
{ ssr: false }
)
const TranslationJoinPage = ({ page }) => {
const { t } = useTranslation(namespaces)
const title = t('translation:joinATranslationTeam')
return (
<PageWrapper {...page} layout={Layout}>
<div className="max-w-4xl mx-auto p-4 mt-4">
<Breadcrumbs
crumbs={[
{ s: 'translation', t: t('translation:translation') },
{ s: 'translation/join', t: title },
]}
title={title}
/>
<h1>{title}</h1>
<p>
{t('translation:joinIntro')}
<br />
{t('translation:thatIsAwesome')} {t('translation:thanksSoMuch')}
</p>
<DynamicAuthWrapper>
<DynamicForm />
</DynamicAuthWrapper>
</div>
</PageWrapper>
)
}
export default TranslationJoinPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['translation/join'],
},
},
}
}

View file

@ -0,0 +1,71 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { BareLayout as Layout } from 'site/components/layouts/bare.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(pageNs), 'translation', 'locales']
/*
* Some things should never generated as SSR
* So for these, we run a dynamic import and disable SSR rendering
*/
const DynamicAuthWrapper = dynamic(
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
{ ssr: false }
)
const DynamicForm = dynamic(
() =>
import('site/components/crowdin/suggest-language.mjs').then((mod) => mod.SuggestLanguageForm),
{ ssr: false }
)
const SuggestLanguagePage = ({ page }) => {
const { t } = useTranslation(namespaces)
const title = t('translation:suggestLanguage')
return (
<PageWrapper {...page} layout={Layout}>
<div className="max-w-4xl mx-auto p-4 mt-4">
<Breadcrumbs
crumbs={[
{ s: 'translation', t: t('translation:translation') },
{ s: 'translation/join', t: title },
]}
title={title}
/>
<h1>{title}</h1>
<p>
{t('translation:suggestIntro')}
<br />
{t('translation:thatIsAwesome')} {t('translation:thanksSoMuch')}
</p>
<DynamicAuthWrapper>
<DynamicForm />
</DynamicAuthWrapper>
</div>
</PageWrapper>
)
}
export default SuggestLanguagePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['translation/suggest-language'],
},
},
}
}

View file

@ -14,5 +14,18 @@ globalRanking: Global ranking
groupByLanguage: Group by language
translator: Translator
words: Words
joinTheTeam: Join the team
joinATranslationTeam: Join a translation team
languageTeam: "{language} Team"
whatTeam: What language team are you joining?
sendMeAnInvite: Send me an invite
pleaseChooseTeam: Please choose a language below so we can send you the correct invite.
successNote: Please check your inbox. You will get an email with an invite code that grants you access to the translation on Crowdin, the online translation platform that we use to translate FreeSewing into multiple languages.
suggestLanguage: Suggest a new language
joinIntro: Looking to join a FreeSewing translation team?
thatIsAwesome: That is awesome.
thanksSoMuch: Thanks so much.
suggestIntro: Looking to add a new language to FreeSewing?
pleaseMotivate: Please complete the form below so we can review your suggestion.

View file

@ -16,7 +16,7 @@ export const siteConfig = {
dataset: 'site-content',
apiVersion: '2023-06-17',
},
languages: ['en', 'es', 'de', 'fr', 'nl'],
languagesWip: ['uk'],
languages: ['en', 'es', 'de', 'fr', 'nl', 'uk'],
languagesWip: [],
site: 'FreeSewing.org',
}

View file

@ -5,11 +5,14 @@ export const ChoiceButton = ({
icon = null,
color = 'secondary',
active = false,
noMargin = false,
}) => (
<button
onClick={onClick}
className={`
flex flex-col flex-nowrap items-start justify-start gap-2 pt-2 pb-4 h-auto w-full mt-3
flex flex-col flex-nowrap items-start justify-start gap-2 pt-2 pb-4 h-auto w-full ${
noMargin ? '' : 'mt-3'
}
btn btn-${color} btn-ghost border border-${color}
hover:bg-opacity-20 hover:bg-${color} hover:border hover:border-${color}
${active ? 'bg-' + color + ' bg-opacity-20 border border-' + color : ''}

View file

@ -287,6 +287,20 @@ Backend.prototype.createIssue = async function (data) {
return responseHandler(await api.post(`/issues`, data), 201)
}
/*
* Send translation invite
*/
Backend.prototype.sendTranslatorInvite = async function (language) {
return responseHandler(await api.post(`/flows/translator-invite/jwt`, { language }, this.auth))
}
/*
* Send language suggestion
*/
Backend.prototype.sendLanguageSuggestion = async function (data) {
return responseHandler(await api.post(`/flows/language-suggestion/jwt`, data, this.auth))
}
export function useBackend(token = false) {
/*
* This backend object is what we'll end up returning