translate /streamer

pull/6012/head
Thibault Duplessis 2020-02-09 14:37:40 -06:00
parent 847bd8ea6e
commit efe708f45a
16 changed files with 216 additions and 104 deletions

View File

@ -129,7 +129,7 @@ final class Streamer(
private def AsStreamer(f: StreamerModel.WithUser => Fu[Result])(implicit ctx: Context) =
ctx.me.fold(notFound) { me =>
api.find(get("u").ifTrue(isGranted(_.Streamers)) | me.id) flatMap {
_.fold(Ok(html.streamer.bits.create(me)).fuccess)(f)
_.fold(Ok(html.streamer.bits.create).fuccess)(f)
}
}

View File

@ -8,9 +8,7 @@ import lila.app.ui.ScalatagsTemplate._
import lila.i18n.{ I18nDb, I18nKey, JsDump, LangList, TimeagoLocales, Translator }
import lila.user.UserContext
trait I18nHelper extends HasEnv {
implicit def ctxLang(implicit ctx: UserContext): Lang = ctx.lang
trait I18nHelper extends HasEnv with UserContext.ToLang {
def transKey(key: String, db: I18nDb.Ref, args: Seq[Any] = Nil)(implicit lang: Lang): Frag =
Translator.frag.literal(key, db, args, lang)

View File

@ -1,28 +1,32 @@
package views.html.streamer
import play.api.i18n.Lang
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.User
object bits {
object bits extends Context.ToLang {
def create(me: User)(implicit ctx: Context) =
import trans.streamer._
def create(implicit ctx: Context) =
views.html.site.message(
title = "Become a lichess streamer",
title = becomeStreamer.txt(),
icon = Some(""),
moreCss = cssTag("streamer.form").some
)(
postForm(cls := "streamer-new", action := routes.Streamer.create)(
h2("Do you have a Twitch or YouTube stream, ", me.username, "?"),
h2(doYouHaveStream()),
br,
br,
bits.rules(),
bits.rules,
br,
br,
p(style := "text-align: center")(
submitButton(cls := "button button-fat text", dataIcon := "")("Here we go!")
submitButton(cls := "button button-fat text", dataIcon := "")(hereWeGo())
)
)
)
@ -34,7 +38,7 @@ object bits {
height := size,
cls := "picture",
src := dbImageUrl(path.value),
alt := s"${u.titleUsername} lichess streamer"
alt := s"${u.titleUsername} Lichess streamer picture"
)
case _ =>
img(
@ -42,13 +46,13 @@ object bits {
height := size,
cls := "default picture",
src := staticUrl("images/placeholder.png"),
alt := "Default streamer picture"
alt := "Default Lichess streamer picture"
)
}
def menu(active: String, s: Option[lila.streamer.Streamer.WithUser])(implicit ctx: Context) =
st.nav(cls := "subnav")(
a(cls := active.active("index"), href := routes.Streamer.index())("All streamers"),
a(cls := active.active("index"), href := routes.Streamer.index())(allStreamers()),
s.map { st =>
frag(
a(cls := active.active("show"), href := routes.Streamer.show(st.streamer.id.value))(
@ -56,10 +60,10 @@ object bits {
),
(ctx.is(st.user) || isGranted(_.Streamers)) option
a(cls := active.active("edit"), href := s"${routes.Streamer.edit}?u=${st.streamer.id.value}")(
"Edit streamer page"
editPage()
)
)
} getOrElse a(href := routes.Streamer.edit)("Your streamer page"),
} getOrElse a(href := routes.Streamer.edit)(yourPage()),
isGranted(_.Streamers) option a(
cls := active.active("requests"),
href := s"${routes.Streamer.index()}?requests=1"
@ -67,7 +71,7 @@ object bits {
a(dataIcon := "", cls := "text", href := "/blog/Wk5z0R8AACMf6ZwN/join-the-lichess-streamer-community")(
"Streamer community"
),
a(href := "/about")("Download streamer kit")
a(href := "/about")(downloadKit())
)
def liveStreams(l: lila.streamer.LiveStreams.WithTitles): Frag =
@ -79,10 +83,9 @@ object bits {
)
}
def contextual(userId: User.ID): Frag =
def contextual(userId: User.ID)(implicit lang: Lang): Frag =
a(cls := "context-streamer text", dataIcon := "", href := routes.Streamer.show(userId))(
usernameOrId(userId),
" is streaming"
xIsStreaming(usernameOrId(userId))
)
object svg {
@ -113,12 +116,19 @@ object bits {
)
}
def rules = ul(cls := "streamer-rules")(
li("Be listed as a lichess streamer."),
li(title := "For example: Blitz battle on lichess.org")(
"Get bumped up the top of the list when you stream with the keyword \"lichess.org\" in the stream title."
def rules(implicit lang: Lang) = ul(cls := "streamer-rules")(
h2(trans.streamer.rules()),
ul(
li(rule1()),
li(rule2()),
li(rule3())
),
li("Notify your lichess followers when you start streaming."),
li("Promote your stream in your games and tournaments.")
h2(perks()),
ul(
li(perk1()),
li(perk2()),
li(perk3()),
li(perk4())
)
)
}

View File

@ -9,7 +9,9 @@ import lila.common.String.html.richText
import controllers.routes
object edit {
object edit extends Context.ToLang {
import trans.streamer._
def apply(
s: lila.streamer.Streamer.WithUserAndStream,
@ -20,7 +22,7 @@ object edit {
val modsOnly = raw("Moderators only").some
views.html.base.layout(
title = s"${s.user.titleUsername} streamer page",
title = s"${s.user.titleUsername} ${lichessStreamer.txt()}",
moreCss = cssTag("streamer.form"),
moreJs = jsTag("streamer.form.js")
) {
@ -30,19 +32,19 @@ object edit {
if (ctx.is(s.user))
div(cls := "streamer-header")(
if (s.streamer.hasPicture)
a(target := "_blank", href := routes.Streamer.picture, title := "Change/delete your picture")(
a(target := "_blank", href := routes.Streamer.picture, title := changePicture.txt())(
bits.pic(s.streamer, s.user)
)
else
div(cls := "picture-create")(
ctx.is(s.user) option
a(target := "_blank", cls := "button", href := routes.Streamer.picture)(
"Upload a picture"
uploadPicture()
)
),
div(cls := "overview")(
h1(s.streamer.name),
bits.rules()
bits.rules
)
)
else views.html.streamer.header(s, none),
@ -53,44 +55,30 @@ object edit {
cls := s"status is${granted ?? "-green"}",
dataIcon := (if (granted) "E" else "")
)(
if (granted)
frag(
"Your stream is approved and listed on ",
a(href := routes.Streamer.index())("lichess streamers list"),
"."
)
if (granted) approved()
else
frag(
if (s.streamer.approval.requested)
frag(
"Your stream is being reviewed by moderators, and will soon be listed on ",
a(href := routes.Streamer.index())("lichess streamers list"),
"."
)
if (s.streamer.approval.requested) pendingReview()
else
frag(
if (s.streamer.completeEnough)
frag(
"When you are ready to be listed on ",
a(href := routes.Streamer.index())("lichess streamers list"),
", ",
whenReady(
postForm(action := routes.Streamer.approvalRequest)(
button(tpe := "submmit", cls := "button", (!ctx.is(s.user)) option disabled)(
"request a moderator review"
button(tpe := "submit", cls := "button", (!ctx.is(s.user)) option disabled)(
requestReview()
)
)
)
else "Please fill in your streamer information, and upload a picture."
else pleaseFillIn()
)
)
),
ctx.is(s.user) option div(cls := "status")(
strong("If your stream is in another language than English"),
", include the correct language tag (",
a(href := "https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes")("2-letter ISO 639-1 code"),
" enclosed in square brackets) at the start of your stream title. ",
"""As examples, include "[RU]" for Russian, "[TR]" for Turkish, "[FR]" for French, etc. """,
"If your stream is in English, there is no need to include a language tag."
anotherLanguage(
a(href := "https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes")(
"2-letter ISO 639-1 code"
)
)
),
modData.map {
case (log, notes) =>
@ -135,13 +123,13 @@ object edit {
form3.split(
form3.checkbox(
form("approval.granted"),
raw("Publish on the streamers list"),
frag("Publish on the streamers list"),
help = modsOnly,
half = true
),
form3.checkbox(
form("approval.requested"),
raw("Active approval request"),
frag("Active approval request"),
help = modsOnly,
half = true
)
@ -149,21 +137,21 @@ object edit {
form3.split(
form3.checkbox(
form("approval.chat"),
raw("Embed stream chat too"),
frag("Embed stream chat too"),
help = modsOnly,
half = true
),
if (granted)
form3.checkbox(
form("approval.featured"),
raw("Feature on lichess homepage"),
frag("Feature on lichess homepage"),
help = modsOnly,
half = true
)
else
form3.checkbox(
form("approval.ignored"),
raw("Ignore further approval requests"),
frag("Ignore further approval requests"),
help = modsOnly,
half = true
)
@ -173,39 +161,39 @@ object edit {
form3.split(
form3.group(
form("twitch"),
raw("Your Twitch username or URL"),
help = raw("Optional. Leave empty if none").some,
twitchUsername(),
help = optionalOrEmpty().some,
half = true
)(form3.input(_)),
form3.group(
form("youTube"),
raw("Your YouTube channel ID or URL"),
help = raw("Optional. Leave empty if none").some,
youtubeChannel(),
help = optionalOrEmpty().some,
half = true
)(form3.input(_))
),
form3.split(
form3.group(
form("name"),
raw("Your streamer name on lichess"),
help = raw("Keep it short: 20 characters max").some,
streamerName(),
help = keepItShort(20).some,
half = true
)(form3.input(_)),
form3.checkbox(
form("listed"),
raw("Visible on the streamers page"),
help = raw("When approved by moderators").some,
visibility(),
help = whenApproved().some,
half = true
)
),
form3.group(
form("headline"),
raw("Headline"),
help = raw("In one sentence, tell us about your stream").some
headline(),
help = tellUsAboutTheStream().some
)(form3.input(_)),
form3.group(form("description"), raw("Long description"))(form3.textarea(_)(rows := 10)),
form3.group(form("description"), longDescription())(form3.textarea(_)(rows := 10)),
form3.actions(
a(href := routes.Streamer.show(s.user.username))("Cancel"),
a(href := routes.Streamer.show(s.user.username))(trans.cancel()),
form3.submit(trans.apply())
)
)

View File

@ -7,6 +7,8 @@ import lila.app.ui.ScalatagsTemplate._
object header {
import trans.streamer._
def apply(s: lila.streamer.Streamer.WithUserAndStream, following: Option[Boolean])(implicit ctx: Context) =
div(cls := "streamer-header")(
bits.pic(s.streamer, s.user),
@ -40,19 +42,16 @@ object header {
a(cls := "service lichess", href := routes.User.show(s.user.username))(
bits.svg.lichess,
" ",
s"lichess.org/@/${s.user.username}"
routes.User.show(s.user.username).url
)
),
div(cls := "ats")(
s.stream.map { s =>
p(cls := "at")(
"Currently streaming: ",
strong(s.status)
)
p(cls := "at")(currentlyStreaming(strong(s.status)))
} getOrElse frag(
p(cls := "at")(trans.lastSeenActive(momentFromNow(s.streamer.seenAt))),
s.streamer.liveAt.map { liveAt =>
p(cls := "at")("Last stream ", momentFromNow(liveAt))
p(cls := "at")(lastStream(momentFromNow(liveAt)))
}
)
),

View File

@ -8,6 +8,8 @@ import lila.common.paginator.Paginator
object index {
import trans.streamer._
private val dataDedup = attr("data-dedup")
def apply(
@ -16,7 +18,7 @@ object index {
requests: Boolean
)(implicit ctx: Context) = {
val title = if (requests) "Streamer approval requests" else "Lichess streamers"
val title = if (requests) "Streamer approval requests" else lichessStreamers.txt()
def widget(s: lila.streamer.Streamer.WithUser, stream: Option[lila.streamer.Stream]) = frag(
a(
@ -26,7 +28,7 @@ object index {
else routes.Streamer.show(s.user.username).url
}
),
stream.isDefined option span(cls := "ribbon")(span("LIVE!")),
stream.isDefined option span(cls := "ribbon")(span(trans.streamer.live())),
bits.pic(s.streamer, s.user),
div(cls := "overview")(
h1(dataIcon := "")(titleTag(s.user.title), stringValueFrag(s.streamer.name)),
@ -52,13 +54,12 @@ object index {
div(cls := "ats")(
stream.map { s =>
p(cls := "at")(
"Currently streaming: ",
strong(s.status)
currentlyStreaming(strong(s.status))
)
} getOrElse frag(
p(cls := "at")(trans.lastSeenActive(momentFromNow(s.streamer.seenAt))),
s.streamer.liveAt.map { liveAt =>
p(cls := "at")("Last stream ", momentFromNow(liveAt))
p(cls := "at")(lastStream(momentFromNow(liveAt)))
}
)
)

View File

@ -7,14 +7,16 @@ import lila.app.ui.ScalatagsTemplate._
object picture {
import trans.streamer._
def apply(s: lila.streamer.Streamer.WithUser, error: Option[String] = None)(implicit ctx: Context) =
views.html.base.layout(
title = s"${s.user.titleUsername} streamer picture",
title = xStreamerPicture.txt(),
moreJs = jsTag("streamer.form.js"),
moreCss = cssTag("streamer.form")
) {
main(cls := "streamer-picture small-page box")(
h1(userLink(s.user), " streamer picture"),
h1(xStreamerPicture(userLink(s.user))),
div(cls := "picture_wrap")(bits.pic(s.streamer, s.user, 250)),
div(cls := "forms")(
error.map { badTag(_) },
@ -23,16 +25,16 @@ object picture {
enctype := "multipart/form-data",
cls := "upload"
)(
p("Max size: ", lila.db.Photographer.uploadMaxMb, "MB."),
p(maxSize(s"${lila.db.Photographer.uploadMaxMb}MB.")),
form3.file.image("picture"),
submitButton(cls := "button")("Upload profile picture")
submitButton(cls := "button")(uploadPicture())
),
s.streamer.hasPicture option
postForm(action := routes.Streamer.pictureDelete, cls := "delete")(
submitButton(cls := "button button-red")("Delete profile picture")
submitButton(cls := "button button-red")(deletePicture())
),
div(cls := "cancel")(
a(href := routes.Streamer.edit, cls := "text", dataIcon := "I")("Return to streamer page form")
a(href := routes.Streamer.edit, cls := "text", dataIcon := "I")(trans.cancel())
)
)
)

View File

@ -9,6 +9,8 @@ import lila.streamer.Stream.YouTube
object show {
import trans.streamer._
def apply(
s: lila.streamer.Streamer.WithUserAndStream,
activities: Vector[lila.activity.ActivityView],
@ -68,14 +70,8 @@ method:'post'
bits.menu("show", s.withoutStream.some),
a(cls := "ads-vulnerable blocker none button button-metal", href := "https://getublockorigin.com")(
i(dataIcon := ""),
strong("Install a malware blocker!"),
"Be safe from ads and trackers",
br,
"infesting Twitch and YouTube.",
br,
"Lichess recommends uBlock Origin",
br,
"which is free and open-source."
strong(installBlocker()),
beSafe()
)
),
div(cls := "page-menu__content")(
@ -96,7 +92,7 @@ method:'post'
frame.allowfullscreen
)
)
} getOrElse div(cls := "box embed")(div(cls := "nostream")("OFFLINE"))
} getOrElse div(cls := "box embed")(div(cls := "nostream")(offline()))
},
div(cls := "box streamer")(
views.html.streamer.header(s, following.some),

View File

@ -1,6 +1,8 @@
package views.html
package study
import play.api.i18n.Lang
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
@ -65,7 +67,7 @@ object bits {
)
)
def streamers(streams: List[lila.streamer.Stream]) =
def streamers(streams: List[lila.streamer.Stream])(implicit lang: Lang) =
streams.nonEmpty option div(cls := "streamers none")(
streams.map { s =>
views.html.streamer.bits.contextual(s.streamer.userId)

View File

@ -2,7 +2,7 @@ const fs = require('fs-extra');
const parseString = require('xml2js').parseString;
const baseDir = 'translation/source';
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast'.split(' ');
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer'.split(' ');
function ucfirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);

View File

@ -76,7 +76,7 @@ lazy val i18n = module("i18n",
MessageCompiler(
sourceDir = new File("translation/source"),
destDir = new File("translation/dest"),
dbs = List("site", "arena", "emails", "learn", "activity", "coordinates", "study", "class", "contact", "patron", "coach", "broadcast"),
dbs = List("site", "arena", "emails", "learn", "activity", "coordinates", "study", "class", "contact", "patron", "coach", "broadcast", "streamer"),
compileTo = (sourceManaged in Compile).value / "messages"
)
}.taskValue,

View File

@ -116,4 +116,8 @@ object Context {
def apply[A](userContext: BodyUserContext[A], pageData: PageData): BodyContext[A] =
new BodyContext(userContext, pageData)
trait ToLang {
implicit def ctxLang(implicit ctx: Context): Lang = ctx.lang
}
}

View File

@ -17,6 +17,7 @@ object I18nDb {
case object Patron extends Ref
case object Coach extends Ref
case object Broadcast extends Ref
case object Streamer extends Ref
val site: Messages = lila.i18n.db.site.Registry.load
val arena: Messages = lila.i18n.db.arena.Registry.load
@ -30,6 +31,7 @@ object I18nDb {
val patron: Messages = lila.i18n.db.patron.Registry.load
val coach: Messages = lila.i18n.db.coach.Registry.load
val broadcast: Messages = lila.i18n.db.broadcast.Registry.load
val streamer: Messages = lila.i18n.db.streamer.Registry.load
def apply(ref: Ref): Messages = ref match {
case Site => site
@ -44,6 +46,7 @@ object I18nDb {
case Patron => patron
case Coach => coach
case Broadcast => broadcast
case Streamer => streamer
}
val langs: Set[Lang] = site.keys.toSet

View File

@ -1,7 +1,7 @@
// Generated with bin/trans-dump.js
package lila.i18n
import I18nDb.{ Activity, Arena, Broadcast, Clas, Coach, Contact, Coordinates, Emails, Learn, Patron, Site, Study }
import I18nDb.{ Activity, Arena, Broadcast, Clas, Coach, Contact, Coordinates, Emails, Learn, Patron, Site, Streamer, Study }
// format: OFF
object I18nKeys {
@ -1433,11 +1433,58 @@ val `sourceUrlHelp` = new Translated("sourceUrlHelp", Broadcast)
val `roundNumber` = new Translated("roundNumber", Broadcast)
val `startDate` = new Translated("startDate", Broadcast)
val `startDateHelp` = new Translated("startDateHelp", Broadcast)
val `throttleSeconds` = new Translated("throttleSeconds", Broadcast)
val `throttleSecondsHelp` = new Translated("throttleSecondsHelp", Broadcast)
val `credits` = new Translated("credits", Broadcast)
val `cloneBroadcast` = new Translated("cloneBroadcast", Broadcast)
val `resetBroadcast` = new Translated("resetBroadcast", Broadcast)
}
object streamer {
val `lichessStreamers` = new Translated("lichessStreamers", Streamer)
val `lichessStreamer` = new Translated("lichessStreamer", Streamer)
val `live` = new Translated("live", Streamer)
val `offline` = new Translated("offline", Streamer)
val `currentlyStreaming` = new Translated("currentlyStreaming", Streamer)
val `lastStream` = new Translated("lastStream", Streamer)
val `becomeStreamer` = new Translated("becomeStreamer", Streamer)
val `doYouHaveStream` = new Translated("doYouHaveStream", Streamer)
val `hereWeGo` = new Translated("hereWeGo", Streamer)
val `allStreamers` = new Translated("allStreamers", Streamer)
val `editPage` = new Translated("editPage", Streamer)
val `yourPage` = new Translated("yourPage", Streamer)
val `downloadKit` = new Translated("downloadKit", Streamer)
val `xIsStreaming` = new Translated("xIsStreaming", Streamer)
val `rules` = new Translated("rules", Streamer)
val `rule1` = new Translated("rule1", Streamer)
val `rule2` = new Translated("rule2", Streamer)
val `rule3` = new Translated("rule3", Streamer)
val `perks` = new Translated("perks", Streamer)
val `perk1` = new Translated("perk1", Streamer)
val `perk2` = new Translated("perk2", Streamer)
val `perk3` = new Translated("perk3", Streamer)
val `perk4` = new Translated("perk4", Streamer)
val `approved` = new Translated("approved", Streamer)
val `pendingReview` = new Translated("pendingReview", Streamer)
val `pleaseFillIn` = new Translated("pleaseFillIn", Streamer)
val `whenReady` = new Translated("whenReady", Streamer)
val `requestReview` = new Translated("requestReview", Streamer)
val `anotherLanguage` = new Translated("anotherLanguage", Streamer)
val `twitchUsername` = new Translated("twitchUsername", Streamer)
val `optionalOrEmpty` = new Translated("optionalOrEmpty", Streamer)
val `youtubeChannel` = new Translated("youtubeChannel", Streamer)
val `streamerName` = new Translated("streamerName", Streamer)
val `visibility` = new Translated("visibility", Streamer)
val `whenApproved` = new Translated("whenApproved", Streamer)
val `headline` = new Translated("headline", Streamer)
val `tellUsAboutTheStream` = new Translated("tellUsAboutTheStream", Streamer)
val `longDescription` = new Translated("longDescription", Streamer)
val `xStreamerPicture` = new Translated("xStreamerPicture", Streamer)
val `changePicture` = new Translated("changePicture", Streamer)
val `uploadPicture` = new Translated("uploadPicture", Streamer)
val `deletePicture` = new Translated("deletePicture", Streamer)
val `maxSize` = new Translated("maxSize", Streamer)
val `installBlocker` = new Translated("installBlocker", Streamer)
val `beSafe` = new Translated("beSafe", Streamer)
val `keepItShort` = new Translated("keepItShort", Streamer)
}
}

View File

@ -79,4 +79,8 @@ object UserContext {
lang: Lang
): BodyUserContext[A] =
new BodyUserContext(req, me, impersonatedBy, lang)
trait ToLang {
implicit def ctxLang(implicit ctx: UserContext): Lang = ctx.lang
}
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="lichessStreamers">Lichess streamers</string>
<string name="lichessStreamer">Lichess streamer</string>
<string name="live">LIVE!</string>
<string name="offline">OFFLINE</string>
<string name="currentlyStreaming">Currently streaming: %s</string>
<string name="lastStream">Last stream %s</string>
<string name="becomeStreamer">Become a Lichess streamer</string>
<string name="doYouHaveStream">Do you have a Twitch or YouTube stream?</string>
<string name="hereWeGo">Here we go!</string>
<string name="allStreamers">All streamers</string>
<string name="editPage">Edit streamer page</string>
<string name="yourPage">Your streamer page</string>
<string name="downloadKit">Download streamer kit</string>
<string name="xIsStreaming">%s is streaming</string>
<string name="rules">Streaming rules</string>
<string name="rule1">Include the keyword \"lichess.org\" in your stream title when you stream on Lichess.</string>
<string name="rule2">Remove the keyword when you stream non-Lichess stuff.</string>
<string name="rule3">Lichess will detect your stream automatically and enable the following perks:</string>
<string name="perks">Benefits of streaming with the keyword</string>
<string name="perk1">Get a flaming streamer icon on your Lichess profile.</string>
<string name="perk2">Get bumped up the top of the streamers list.</string>
<string name="perk3">Notify your Lichess followers.</string>
<string name="perk4">Show your stream in your games, tournaments and studies.</string>
<string name="approved">Your stream is approved.</string>
<string name="pendingReview">Your stream is being reviewed by moderators.</string>
<string name="pleaseFillIn">Please fill in your streamer information, and upload a picture.</string>
<string name="whenReady">When you are ready to be listed as a lichess streamer, %s</string>
<string name="requestReview">request a moderator review</string>
<string name="anotherLanguage">If your stream is in another language than English, include the correct language tag (%s), enclosed in square brackets, at the start of your stream title.
As examples, include "[RU]" for Russian, "[TR]" for Turkish, "[FR]" for French, etc.
Put it in the stream title in the Twitch or YouTube page, not in the Lichess stream settings.
If your stream is in English, there is no need to include a language tag.</string>
<string name="twitchUsername">Your Twitch username or URL</string>
<string name="optionalOrEmpty">Optional. Leave empty if none</string>
<string name="youtubeChannel">Your YouTube channel ID or URL</string>
<string name="streamerName">Your streamer name on Lichess</string>
<plurals name="keepItShort">
<item quantity="one">Keep it short: %s character max</item>
<item quantity="other">Keep it short: %s characters max</item>
</plurals>
<string name="visibility">Visible on the streamers page</string>
<string name="whenApproved">When approved by moderators</string>
<string name="headline">Headline</string>
<string name="tellUsAboutTheStream">In one sentence, tell us about your stream</string>
<string name="longDescription">Long description</string>
<string name="xStreamerPicture">%s streamer picture</string>
<string name="changePicture">Change/delete your picture</string>
<string name="uploadPicture">Upload a picture</string>
<string name="deletePicture">Delete picture</string>
<string name="maxSize">Max size: %s</string>
<string name="installBlocker">Install a malware blocker!</string>
<string name="beSafe">Be safe from ads and trackers
infesting Twitch and YouTube.
Lichess recommends uBlock Origin
which is free and open-source.</string>
</resources>