Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Genesys PGR
Genesys Backend
Commits
f295095d
Commit
f295095d
authored
Oct 29, 2014
by
Alexander Basov
Committed by
Matija Obreza
Nov 10, 2014
Browse files
Transifex integration for review
parent
f1cf8d1b
Changes
11
Hide whitespace changes
Inline
Side-by-side
src/main/java/org/genesys2/server/model/impl/Article.java
View file @
f295095d
...
...
@@ -21,6 +21,7 @@ import org.hibernate.annotations.Type;
import
javax.persistence.*
;
import
java.util.Calendar
;
import
java.util.List
;
@Entity
@Table
(
name
=
"article"
)
...
...
@@ -53,7 +54,8 @@ public class Article extends AuditedModel {
@Column
(
length
=
100000
)
private
String
body
;
private
boolean
transifexed
=
true
;
@OneToMany
(
mappedBy
=
"article"
,
fetch
=
FetchType
.
LAZY
,
cascade
=
CascadeType
.
REMOVE
,
orphanRemoval
=
true
)
private
List
<
TransifexTranslation
>
transifexTranslations
;
@Temporal
(
TemporalType
.
TIMESTAMP
)
private
Calendar
postDate
;
...
...
@@ -114,11 +116,12 @@ public class Article extends AuditedModel {
this
.
lang
=
lang
;
}
public
boolean
isTransifexed
()
{
return
transifex
ed
;
public
List
<
TransifexTranslation
>
getTranslations
()
{
return
transifex
Translations
;
}
public
void
setTrans
ifexed
(
boolean
transifexed
)
{
this
.
transifex
ed
=
trans
ifexed
;
public
void
setTrans
lations
(
List
<
TransifexTranslation
>
translations
)
{
this
.
transifex
Translations
=
trans
lations
;
}
}
src/main/java/org/genesys2/server/model/impl/TransifexTranslation.java
0 → 100644
View file @
f295095d
package
org.genesys2.server.model.impl
;
import
org.genesys2.server.model.BusinessModel
;
import
javax.persistence.*
;
@Entity
@Table
(
name
=
"transifextranslation"
)
public
class
TransifexTranslation
extends
BusinessModel
{
private
static
final
long
serialVersionUID
=
1L
;
@Column
private
String
translatedLang
;
@Column
private
boolean
transifexed
;
@ManyToOne
(
cascade
=
CascadeType
.
PERSIST
)
@JoinColumn
(
name
=
"article_id"
,
nullable
=
false
)
private
Article
article
;
public
String
getTranslatedLang
()
{
return
translatedLang
;
}
public
void
setTranslatedLang
(
String
translatedLang
)
{
this
.
translatedLang
=
translatedLang
;
}
public
boolean
isTransifexed
()
{
return
transifexed
;
}
public
void
setTransifexed
(
boolean
transifexed
)
{
this
.
transifexed
=
transifexed
;
}
public
Article
getArticle
()
{
return
article
;
}
public
void
setArticle
(
Article
article
)
{
this
.
article
=
article
;
}
}
src/main/java/org/genesys2/server/persistence/domain/TransifextranslationRepository.java
0 → 100644
View file @
f295095d
package
org.genesys2.server.persistence.domain
;
import
org.genesys2.server.model.impl.TransifexTranslation
;
import
org.springframework.data.jpa.repository.JpaRepository
;
import
org.springframework.data.jpa.repository.Query
;
import
org.springframework.data.repository.query.Param
;
import
java.util.List
;
public
interface
TransifextranslationRepository
extends
JpaRepository
<
TransifexTranslation
,
Long
>{
@Query
(
"select tt from TransifexTranslation tt join tt.article a where a.id = :articleId"
)
List
<
TransifexTranslation
>
findByArticleId
(
@Param
(
"articleId"
)
Long
articleId
);
}
src/main/java/org/genesys2/server/service/TransifexTranslationService.java
0 → 100644
View file @
f295095d
package
org.genesys2.server.service
;
import
org.genesys2.server.model.impl.TransifexTranslation
;
import
java.util.List
;
public
interface
TransifexTranslationService
{
TransifexTranslation
getTransifexTranslation
(
Long
id
);
void
save
(
String
articleName
,
TransifexTranslation
transifexTranslations
);
void
deleteTransifexTranslation
(
Long
id
);
List
<
TransifexTranslation
>
findAll
();
List
<
TransifexTranslation
>
findAllByArticleId
(
Long
articleId
);
}
src/main/java/org/genesys2/server/service/impl/TransifexTranslationServiceImpl.java
0 → 100644
View file @
f295095d
package
org.genesys2.server.service.impl
;
import
org.genesys2.server.model.impl.Article
;
import
org.genesys2.server.model.impl.ClassPK
;
import
org.genesys2.server.model.impl.TransifexTranslation
;
import
org.genesys2.server.persistence.domain.ArticleRepository
;
import
org.genesys2.server.persistence.domain.ClassPKRepository
;
import
org.genesys2.server.persistence.domain.TransifextranslationRepository
;
import
org.genesys2.server.service.TransifexTranslationService
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.context.i18n.LocaleContextHolder
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
java.util.List
;
@Service
@Transactional
public
class
TransifexTranslationServiceImpl
implements
TransifexTranslationService
{
@Autowired
private
TransifextranslationRepository
transifextranslationRepository
;
@Autowired
private
ArticleRepository
articleRepository
;
@Autowired
private
ClassPKRepository
classPKRepository
;
@Override
public
TransifexTranslation
getTransifexTranslation
(
Long
id
)
{
return
transifextranslationRepository
.
getOne
(
id
);
}
@Override
public
void
save
(
String
articleName
,
TransifexTranslation
transifexTranslations
)
{
ClassPK
articleClassPk
=
classPKRepository
.
findByClassName
(
Article
.
class
.
getName
());
Article
article
=
articleRepository
.
findByClassPkAndTargetIdAndSlugAndLang
(
articleClassPk
,
null
,
articleName
,
LocaleContextHolder
.
getLocale
().
getLanguage
());
if
(
article
!=
null
)
{
transifexTranslations
.
setArticle
(
article
);
transifextranslationRepository
.
save
(
transifexTranslations
);
}
}
@Override
public
void
deleteTransifexTranslation
(
Long
id
)
{
transifextranslationRepository
.
delete
(
id
);
}
@Override
public
List
<
TransifexTranslation
>
findAll
()
{
return
transifextranslationRepository
.
findAll
();
}
@Override
public
List
<
TransifexTranslation
>
findAllByArticleId
(
Long
articleId
)
{
return
transifextranslationRepository
.
findByArticleId
(
articleId
);
}
}
src/main/java/org/genesys2/server/servlet/controller/transifex/TransifexAPIController.java
View file @
f295095d
package
org.genesys2.server.servlet.controller.transifex
;
import
org.apache.commons.codec.binary.Base64
;
import
org.dom4j.Document
;
import
org.dom4j.DocumentException
;
import
org.dom4j.DocumentHelper
;
import
org.dom4j.io.HTMLWriter
;
import
org.dom4j.io.OutputFormat
;
import
org.apache.commons.io.IOUtils
;
import
org.genesys2.server.model.impl.Article
;
import
org.genesys2.server.model.impl.TransifexTranslation
;
import
org.genesys2.server.service.ContentService
;
import
org.genesys2.server.service.TransifexTranslationService
;
import
org.genesys2.server.servlet.controller.BaseController
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
...
...
@@ -25,9 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import
org.springframework.web.client.HttpClientErrorException
;
import
org.springframework.web.client.RestTemplate
;
import
java.io.File
;
import
java.io.FileWriter
;
import
java.io.IOException
;
import
java.io.*
;
@Controller
@RequestMapping
(
value
=
"/transifex"
)
...
...
@@ -36,6 +32,9 @@ public class TransifexAPIController extends BaseController{
@Autowired
private
ContentService
contentService
;
@Autowired
private
TransifexTranslationService
translationService
;
@Value
(
"${transifex.project}"
)
private
String
projectSlug
;
...
...
@@ -45,23 +44,24 @@ public class TransifexAPIController extends BaseController{
@Value
(
"${transifex.password}"
)
private
String
transifexPassord
;
@Value
(
"${transifex.min.translated}"
)
private
String
minTranslatedValue
;
@Value
(
"${transifex.base.api.url}"
)
private
String
baseApiURL
;
private
RestTemplate
template
;
private
HttpHeaders
headers
;
@Value
(
"${transifex.content.template}"
)
private
String
contentTemplate
;
@Value
(
"${transifex.min.translated}"
)
private
int
transifexMinTranslated
;
private
RestTemplate
template
=
new
RestTemplate
();
@RequestMapping
(
value
=
"/deleteContent/{slug}"
)
public
String
deleteResource
(
@PathVariable
String
slug
,
Model
model
)
{
//make authentication for Transifex
basicAuthentication
();
HttpHeaders
headers
=
basicAuthentication
();
Article
article
=
contentService
.
getGlobalArticle
(
slug
,
getLocale
());
//
if
(
resourceExists
(
article
))
{
StringBuilder
url
=
new
StringBuilder
();
url
.
append
(
"project/"
);
...
...
@@ -74,37 +74,56 @@ public class TransifexAPIController extends BaseController{
if
(
response
.
getStatusCode
().
value
()
==
204
){
model
.
addAttribute
(
"result"
,
"deleted ok"
);
}
}
else
{
model
.
addAttribute
(
"result"
,
"not exists"
);
}
model
.
addAttribute
(
"title"
,
article
.
getTitle
());
model
.
addAttribute
(
"article"
,
article
);
return
"/content/article-edit"
;
}
@RequestMapping
(
value
=
"/hooks"
,
method
=
RequestMethod
.
POST
)
/**
* Use "magic" SHA1 hash value just for some security purposes
*
* Note: the very same value should be in Transifex preferences
*
* @see classpath:spring/spring-security.xml
*/
@RequestMapping
(
value
=
"/hooks/40874cca86ca396169a5f4e6ebf6e4bf7199c4e7"
,
method
=
RequestMethod
.
POST
)
public
void
webHookHandle
(
@RequestParam
(
value
=
"project"
)
String
projectSlug
,
@RequestParam
(
value
=
"resource"
)
String
resource
,
@RequestParam
(
value
=
"language"
)
String
language
,
@RequestParam
(
value
=
"translated"
)
int
translated
){
System
.
out
.
println
(
projectSlug
+
" done \n"
);
@RequestParam
(
value
=
"translated"
)
int
translatedPercentage
,
Model
model
){
//TODO may be revisit this
//currently we do this due to nature of Transifex hook
String
articleName
=
resource
.
split
(
"-"
)[
1
];
if
(
translatedPercentage
==
transifexMinTranslated
){
TransifexTranslation
translation
=
new
TransifexTranslation
();
translation
.
setTransifexed
(
true
);
translation
.
setTranslatedLang
(
language
);
translationService
.
save
(
articleName
,
translation
);
}
}
@RequestMapping
(
value
=
"/translate/{slug}/{lang}"
,
method
=
RequestMethod
.
GET
)
public
String
translateArticles
(
@PathVariable
String
lang
,
@PathVariable
String
slug
,
Model
model
)
throws
DocumentException
{
public
String
translateArticles
(
@PathVariable
String
lang
,
@PathVariable
String
slug
,
Model
model
)
{
//make authentication for Transifex
basicAuthentication
();
HttpHeaders
headers
=
basicAuthentication
();
//Extract article from database we need
Article
article
=
contentService
.
getGlobalArticle
(
slug
,
getLocale
());
StringBuilder
url
=
new
StringBuilder
()
;
url
.
append
(
"project/"
)
;
url
.
append
(
projectSlug
)
;
url
.
append
(
"/resource/"
)
;
url
.
append
(
"article-"
)
;
url
.
append
(
slug
)
;
url
.
append
(
"/translation/"
)
;
url
.
append
(
lang
)
;
url
.
append
(
"/?file"
);
StringBuilder
url
=
new
StringBuilder
()
.
append
(
"project/"
)
.
append
(
projectSlug
)
.
append
(
"/resource/"
)
.
append
(
"article-"
)
.
append
(
slug
)
.
append
(
"/translation/"
)
.
append
(
lang
)
.
append
(
"/?file"
);
HttpEntity
<
String
>
request
=
new
HttpEntity
<>(
headers
);
...
...
@@ -113,10 +132,7 @@ public class TransifexAPIController extends BaseController{
String
title
=
response
.
getBody
().
split
(
"<title>"
)[
1
].
split
(
"</title>"
)[
0
];
String
body
=
response
.
getBody
().
split
(
"<body>"
)[
1
].
split
(
"</body>"
)[
0
];
article
.
setTitle
(
title
);
article
.
setBody
(
body
);
contentService
.
updateArticle
(
article
.
getId
(),
article
.
getSlug
(),
article
.
getTitle
(),
article
.
getBody
());
contentService
.
updateArticle
(
article
.
getId
(),
article
.
getSlug
(),
title
,
body
);
model
.
addAttribute
(
"title"
,
article
.
getTitle
());
model
.
addAttribute
(
"article"
,
article
);
...
...
@@ -125,93 +141,78 @@ public class TransifexAPIController extends BaseController{
}
@RequestMapping
(
value
=
"/postContent/{slug}"
)
public
String
postResourceToTransifex
(
@PathVariable
String
slug
,
Model
model
)
throws
IOException
,
DocumentException
{
//We want to sent .xhtml file to transifex, for this we use dom4j
public
String
postResourceToTransifex
(
@PathVariable
String
slug
,
Model
model
)
throws
IOException
{
//Make the authentication for Transifex
basicAuthentication
();
HttpHeaders
headers
=
basicAuthentication
();
//Extract article from database we need
Article
article
=
contentService
.
getGlobalArticle
(
slug
,
getLocale
(),
false
);
//Create temporary file articleSlug.xhtml
String
filePath
=
"/home/alexander/Downloads/"
+
article
.
getSlug
()+
".xhtml"
;
File
xhtmlFile
=
new
File
(
filePath
);
xhtmlFile
.
createNewFile
();
//Create dom4j document witch will be containing xhtml content
Document
xhtmlDocument
;
//Define xhtml output format for HTMLWriter
OutputFormat
outputFormat
=
OutputFormat
.
createPrettyPrint
();
outputFormat
.
setXHTML
(
true
);
HTMLWriter
writer
=
new
HTMLWriter
(
new
FileWriter
(
xhtmlFile
),
outputFormat
);
if
(!
resourceExists
(
article
))
{
//We will request like MULTIPART_FORM_DATA
headers
.
setContentType
(
MediaType
.
MULTIPART_FORM_DATA
);
//This is template our xhtml
String
content
=
"<html xmlns=\"http://www.w3.org/1999/xhtml\"> \n"
+
" <head> \n"
+
"<meta http-equiv=\"CONTENT-TYPE\" content=\"text/html; charset=utf8\"/> \n"
+
"<title>"
+
article
.
getTitle
()+
"</title> \n"
+
" </head> \n"
+
" <body>"
+
article
.
getBody
()+
"</body> \n"
+
"</html>"
;
//Parse String content and write it into articleSlug.xhtml file
xhtmlDocument
=
DocumentHelper
.
parseText
(
content
);
writer
.
write
(
xhtmlDocument
);
writer
.
flush
();
//Create Multi value map with all necessary information for request
String
content
=
String
.
format
(
contentTemplate
,
article
.
getTitle
(),
article
.
getBody
());
//Create Multi
// value map with all necessary information for request
MultiValueMap
<
String
,
Object
>
map
=
new
LinkedMultiValueMap
<>();
map
.
add
(
"slug"
,
"article-"
+
slug
);
map
.
add
(
"name"
,
article
.
getTitle
());
map
.
add
(
"i18n_type"
,
"XHTML"
);
Resource
resource
=
new
FileSystemResource
(
filePath
);
map
.
add
(
"content"
,
resource
);
//Create our request entity
HttpEntity
<
MultiValueMap
<
String
,
Object
>>
request
=
new
HttpEntity
<>(
map
,
headers
);
model
.
addAttribute
(
"title"
,
article
.
getTitle
());
model
.
addAttribute
(
"article"
,
article
);
File
tempFile
=
File
.
createTempFile
(
slug
,
".xhtml"
);
//default FileWriter support default encoding only
try
(
BufferedWriter
writer
=
new
BufferedWriter
(
new
FileWriter
(
tempFile
))){
StringBuilder
url
=
new
StringBuilder
();
url
.
append
(
"project/"
);
url
.
append
(
projectSlug
);
url
.
append
(
"/resources/"
);
IOUtils
.
write
(
content
,
writer
);
writer
.
flush
();
Resource
resource
=
new
FileSystemResource
(
tempFile
);
map
.
add
(
"content"
,
resource
);
//Create our request entity
HttpEntity
<
MultiValueMap
<
String
,
Object
>>
request
=
new
HttpEntity
<>(
map
,
headers
);
StringBuilder
url
=
new
StringBuilder
();
url
.
append
(
"project/"
);
url
.
append
(
projectSlug
);
url
.
append
(
"/resources/"
);
try
{
//Send our post request(with .xhtml file) to Transifex
template
.
postForLocation
(
baseApiURL
+
url
,
request
);
model
.
addAttribute
(
"responseFromTransifex"
,
"Resource added"
);
}
catch
(
HttpClientErrorException
e
)
{
model
.
addAttribute
(
"responseFromTransifex"
,
"fail"
);
}
//After all operations, temporary .xhtml file is deleted
xhtmlFile
.
delete
(
);
}
else
{
model
.
addAttribute
(
"responseFromTransifex"
,
"already exists"
);
}
model
.
addAttribute
(
"title"
,
article
.
getTitle
());
model
.
addAttribute
(
"article"
,
article
);
return
"/content/article-edit"
;
}
//Transifex requires Basic HTTP authentication!
//This method make authentication for Transifex
public
void
basicAuthentication
()
{
public
HttpHeaders
basicAuthentication
()
{
template
=
new
RestTemplate
();
headers
=
new
HttpHeaders
();
HttpHeaders
headers
=
new
HttpHeaders
();
String
trasifexCreds
=
trasifexUserName
+
":"
+
transifexPassord
;
byte
[]
transifexCredsBytes
=
trasifexCreds
.
getBytes
();
byte
[]
base64CredsBytes
=
Base64
.
encodeBase64
(
transifexCredsBytes
);
String
base64Creds
=
new
String
(
base64CredsBytes
);
headers
.
add
(
"Authorization"
,
"Basic "
+
base64Creds
);
return
headers
;
}
//This method verify if article already exists in Transifex
public
boolean
resourceExists
(
Article
resource
)
{
//make authentication for Transifex
basicAuthentication
();
HttpHeaders
headers
=
basicAuthentication
();
StringBuilder
url
=
new
StringBuilder
();
url
.
append
(
"project/"
);
url
.
append
(
projectSlug
);
...
...
@@ -228,7 +229,7 @@ public class TransifexAPIController extends BaseController{
return
false
;
}
return
response
.
getStatusCode
().
value
()
==
200
;
return
response
.
getStatusCode
().
value
()
==
HttpStatus
.
OK
.
value
()
;
}
}
src/main/java/org/genesys2/spring/config/SpringDataBaseConfig.java
View file @
f295095d
...
...
@@ -16,8 +16,6 @@
package
org.genesys2.spring.config
;
import
java.util.Properties
;
import
org.apache.tomcat.jdbc.pool.DataSource
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.context.annotation.Bean
;
...
...
@@ -29,6 +27,8 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import
org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter
;
import
org.springframework.transaction.annotation.EnableTransactionManagement
;
import
java.util.Properties
;
@EnableJpaRepositories
(
basePackages
=
{
"org.genesys2.server.persistence.domain"
,
"org.genesys2.server.persistence.acl"
},
entityManagerFactoryRef
=
"entityManagerFactory"
,
transactionManagerRef
=
"transactionManager"
,
repositoryImplementationPostfix
=
"CustomImpl"
)
// @EnableJpaAuditing(auditorAwareRef = "auditorAware")
@EnableTransactionManagement
...
...
src/main/java/org/genesys2/spring/config/SpringProperties.java
View file @
f295095d
...
...
@@ -31,6 +31,7 @@ public class SpringProperties {
final
PropertyPlaceholderConfigurer
propertyPlaceholderConfigurer
=
new
PropertyPlaceholderConfigurer
();
// Need to ignore "genesys.properties" if not found
propertyPlaceholderConfigurer
.
setIgnoreResourceNotFound
(
true
);
propertyPlaceholderConfigurer
.
setFileEncoding
(
"utf-8"
);
propertyPlaceholderConfigurer
.
setLocations
(
new
Resource
[]
{
new
ClassPathResource
(
"application.properties"
),
...
...
src/main/resources/spring/spring-security.xml
View file @
f295095d
...
...
@@ -47,13 +47,23 @@
<!-- And other stuff -->
<sec:http
pattern=
"/webapi/**"
security=
"none"
create-session=
"stateless"
/>
<!--Ignore security for Transifex hook-->
<!--Use "magic" SHA1 hash value just for some security purposes-->
<!--see org.genesys2.server.servlet.controller.transifex.TransifexAPIController#webHookHandle-->
<!--* Note: the very same value should be in Transifex preferences-->
<sec:http
pattern=
"/transifex/hooks/40874cca86ca396169a5f4e6ebf6e4bf7199c4e7"
security=
"none"
create-session=
"stateless"
/>
<!-- Closed page and Authentication filter -->
<sec:http
auto-config=
"true"
use-expressions=
"true"
>
<!-- <intercept-url pattern="/data/**" access="isAuthenticated()" /> -->
<sec:intercept-url
pattern=
"/admin/**"
access=
"hasRole('ADMINISTRATOR')"
/>
<sec:intercept-url
pattern=
"/profile**"
access=
"isAuthenticated()"
/>
<sec:intercept-url
pattern=
"/oauth/authorize"
access=
"isAuthenticated()"
/>
<!--secure transifex URLs-->
<sec:intercept-url
pattern=
"/transifex/**"
access=
"isAuthenticated()"
/>
<!--Override default login and logout pages -->
<sec:form-login
login-page=
"/login"
authentication-failure-url=
"/login?error=1"
login-processing-url=
"/login-attempt"
default-target-url=
"/"
always-use-default-target=
"false"
/>
<sec:session-management
session-fixation-protection=
"migrateSession"
/>
...
...
src/main/resources/spring/spring.properties
View file @
f295095d
...
...
@@ -130,4 +130,12 @@ transifex.project=genesys-dev
transifex.username
=
Alexandr19011990
transifex.password
=
Alexandr19011990
transifex.min.translated
=
80
transifex.base.api.url
=
https://www.transifex.com/api/2/
transifex.base.api.url
=
https://www.transifex.com/api/2/
#TODO perhaps, Velocity template?
transifex.content.template
=
<html xmlns="http://www.w3.org/1999/xhtml">
\
<head>
\
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
\
<title>%s</title>
\
</head>
\
<body>%s</body>
\
</html>
src/main/webapp/WEB-INF/jsp/content/article-edit.jsp
View file @
f295095d
...
...
@@ -21,11 +21,11 @@
<a
href=
"
<c:url
value=
"/transifex/deleteContent/${article.slug}"
/>
"
class=
"btn btn-default"
>
Delete from
Transifex
</a>
</div>
<div>
${resource}
</div>
<div
class=
"form-group"
>
<div
class=
"dropdown"
>
<a
href=
"#"
class=
"dropdown-toggle"
data-toggle=
"dropdown"
><c:out
value=
"Transifex's translate"
/>
<b
class=
"caret"
></b></a>
<ul
class=
"dropdown-menu"
role=
"menu"
>
<ul
class=
"dropdown-menu"
role=
"menu"
id=
"transifiex_langs"
>
<li><a
href=
"${pageContext.request.contextPath}/transifex/translate/${article.slug}/${"
en
"}"
>
English
</a></li>
<li><a
href=
"${pageContext.request.contextPath}/transifex/translate/${article.slug}/${"
es
"}"
>
Spanish
</a></li>
<li><a
href=
"${pageContext.request.contextPath}/transifex/translate/${article.slug}/${"
ar
"}"
>
Arabic
</a></li>
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment