diff --git a/markdown/dev/guides/translation/en.md b/markdown/dev/guides/translation/en.md
index 671961b4300..e4114ae1641 100644
--- a/markdown/dev/guides/translation/en.md
+++ b/markdown/dev/guides/translation/en.md
@@ -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.
-###### [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)
diff --git a/sites/org/components/crowdin/suggest-language.mjs b/sites/org/components/crowdin/suggest-language.mjs
new file mode 100644
index 00000000000..d46187dc497
--- /dev/null
+++ b/sites/org/components/crowdin/suggest-language.mjs
@@ -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) => (
+
+ {name}
+ {i < count - 2 ? ', ' : i < count - 1 ? ' & ' : ' '}
+
+ ))}
+ >
+ )
+}
+
+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 (
+ <>
+
+
Suggestion submitted
+
+ We will get back to you shortly. Thank you for taking an interest in bringing FreeSewing
+ to more people, specifically the {language} community.
+
+
+ >
+ )
+
+ return (
+ <>
+
In what language would you like make FreeSewing available?
+
+ {language ? (
+ <>
+
+ Do you plan to (help) translate FreeSewing to this language yourself?
+
+
+ setHelp(1)} active={help === 1}>
+ I am able and willing to help out with translation
+
+ setHelp(-1)}
+ active={help === -1}
+ >
+ I do not intent to help out with translation
+
+
+ >
+ ) : (
+
+
Are you looking to suggest a language that is not in the list?
+
+ The list of languages above does obviously not include all languages. Instead,
+ it is limimted to the list of langauges that are supported by{' '}
+ , a machine-learning tool that can
+ help translators with suggestions that make for an efficient translation experience.
+
+
+ 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.
+
+
+ If you are committed to translating FreeSewing to a language not in the list above,
+ please .
+
+
+ )}
+ {language && help < 0 && (
+
+
Thank you for your suggestion
+
+ We appreciate that you would like to see FreeSewing translated to {language}.
+
+ However, since you are unable to make a commitment yourself, we will not process your
+ suggestion.
+
+
+ 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}.
+
+
You can then send those people to this same page to suggest {language} again.
+
+ )}
+ {language && help > 0 && (
+ <>
+
Do you have friends who can help?
+
+ setFriends(1)}
+ active={friends === 1}
+ >
+ And they want to help out too!
+
+ setFriends(-1)}
+ active={friends === -1}
+ >
+ But maybe others will join my effort?
+
+
+
+ )
+}
+
+export default SuggestLanguagePage
+
+export async function getStaticProps({ locale }) {
+ return {
+ props: {
+ ...(await serverSideTranslations(locale, namespaces)),
+ page: {
+ locale,
+ path: ['translation/suggest-language'],
+ },
+ },
+ }
+}
diff --git a/sites/org/pages/translation/translation.en.yaml b/sites/org/pages/translation/translation.en.yaml
index fb01f5b9277..11ba65f41a8 100644
--- a/sites/org/pages/translation/translation.en.yaml
+++ b/sites/org/pages/translation/translation.en.yaml
@@ -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.
diff --git a/sites/org/site.config.mjs b/sites/org/site.config.mjs
index 3b2989e45bf..c12cb87a745 100644
--- a/sites/org/site.config.mjs
+++ b/sites/org/site.config.mjs
@@ -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',
}
diff --git a/sites/shared/components/choice-button.mjs b/sites/shared/components/choice-button.mjs
index a10d12e6247..def8f21e5bc 100644
--- a/sites/shared/components/choice-button.mjs
+++ b/sites/shared/components/choice-button.mjs
@@ -5,11 +5,14 @@ export const ChoiceButton = ({
icon = null,
color = 'secondary',
active = false,
+ noMargin = false,
}) => (