post-photo

Announcing GoSheLo!

Goshelo is an easy-to-use tool to organize your translation files, with just a few simple steps. The most straightforward way to use it is to create Google Sheets following this template, paste in the share link, check the requested platforms and click on the magical convert button. A few seconds later you will be provided with .zip file containing all the needed translation files in their basic folder structures.

If clicking and pasting isn’t your style you can easily automatize this process by yourself, but our helpful developers at Wanari did it already, for you!

You can find the following guides below:

Android

The most straightforward way to utilize this tool in Android projects is to create a new java module named buildSrc in your project. This module is going to contain a localizer package with three files: DownloadTask.kt, LocalizerPlugin.kt and LocalizerPluginExtension.kt. You can see the project tree and the detailed files below.

text

DownloadTask.kt

package localizer
 
import okhttp3.Request
import okhttp3.Response
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.IOException
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.util.zip.ZipInputStream
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
 
const val BUFFER_SIZE = 1024
 
open class DownloadTask : DefaultTask() {
 
    init {
        group = "localization"
        description = "Update localization"
    }
 
    private val config by lazy {
        project.extensions.getByType(LocalizerPluginExtension::class.java)
    }
 
    private val workDir by lazy {
        File(project.rootDir, ".localizer/data")
    }
    private val versionFile by lazy {
        File(project.rootDir, ".localizer/.version")
    }
 
    private var lastUpdated: Long = 0L
 
    @TaskAction
    fun downloadTranslations() {
        validateConfig(config)
                .let { (apiUrl, locUrl, module) ->
                    checkForUpdatedTranslations(apiUrl, locUrl)
                            .checkAndFilterResponse()
                            ?.unzipResponseBodyTo(workDir)
                            ?.copyNewTranslationsToResources(workDir, module)
                            ?.updateVersion()
                }
    }
 
    private fun Response.checkAndFilterResponse(): ResponseBody? {
        return when {
            code() == java.net.HttpURLConnection.HTTP_OK && body() != null -> body()
            code() == java.net.HttpURLConnection.HTTP_NOT_MODIFIED -> {
                println("Translations are UP-TO-DATE")
                null
            }
            else -> null // TODO Check if up to date
        }
    }
 
    @Throws(IllegalArgumentException::class)
    private fun validateConfig(config: LocalizerPluginExtension): Triple<String, String, String> {
        println("Validating localization config...")
 
        val apiUrl = config.apiUrl
            ?: throw IllegalArgumentException("localize.apiUrl not specified!")
        val locUrl = config.localizationUrl
            ?: throw IllegalArgumentException("localize.localizationUrl not specified!")
        val module = config.module
            ?: "app".apply { println("localize.module not specified, fallback to \"app\" module") }
        return Triple(apiUrl, locUrl, module)
    }
 
    @Throws(IOException::class)
    private fun checkForUpdatedTranslations(apiUrl: String, locUrl: String): Response {
        lastUpdated = versionFile.lastModified()
 
        if (lastUpdated != 0L) {
            println("Current revision: ${LocalDateTime.ofEpochSecond(Duration.ofMillis(lastUpdated).seconds,
                    0, ZonedDateTime.now().offset)}")
        }
 
        print("Checking for translations... ")
 
        val request = Request.Builder()
                .get()
                .url(HttpUrl.get(apiUrl)
                        .newBuilder()
                        .addQueryParameter("platform", "android")
                        .addQueryParameter("url", locUrl)
                        .addQueryParameter("lastModified", lastUpdated.toString())
                        .build())
                .build()
 
        return try {
            lastUpdated = Instant.now().epochSecond // The moment before the request execution
            val sslContext = SSLContext.getInstance("SSL")
            val trustManager = object : X509TrustManager{
                override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
                }
 
                override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
                }
 
                override fun getAcceptedIssuers(): Array<X509Certificate> {
                    return emptyArray()
                }
            }
            sslContext.init(null, arrayOf(trustManager), SecureRandom())
            OkHttpClient.Builder().sslSocketFactory(sslContext.socketFactory,trustManager).hostnameVerifier { s, sslSession -> true }.build().newCall(request).execute().apply {
                println("COMPLETE")
            }
        } catch (e: IOException) {
            println("FAILED")
            throw e
        }
    }
 
    @Throws(IOException::class)
    private fun ResponseBody.unzipResponseBodyTo(targetDir: File) {
        println("Unzipping translations... ")
 
        try {
            targetDir.deleteRecursively()
            targetDir.mkdir()
 
            use { body ->
                java.util.zip.ZipInputStream(body.byteStream())
                        .unzipTo(targetDir)
            }
 
            println("Unzipping translations... COMPLETE")
        } catch (e: IOException) {
            println("Unzipping translations... FAILED")
            println("Error while unzipping translations:")
            throw e
        }
    }
 
    @Throws(IOException::class)
    private fun ZipInputStream.unzipTo(targetDir: File) = use { zip ->
        var entry = zip.nextEntry
 
        while (entry != null) {
            File(targetDir, entry.name)
                    .apply {
                        println("Creating: ${toPath()}")
                        parentFile.mkdirs()
                        createNewFile()
                        println("Extracting: ${toPath()}")
                    }
                    .outputStream()
                    .use { zip.copyTo(it, BUFFER_SIZE) }
            entry = zip.nextEntry
        }
 
        zip.closeEntry()
    }
 
    @Throws(IOException::class)
    private fun Unit.copyNewTranslationsToResources(workDir: File, module: String) {
        print("Coping translations to $module module's resources... ")
 
        project.subprojects.find { it.name == module }
                ?.let { File(it.projectDir, "/src/main/res") }
                ?.takeIf { it.isDirectory }
                ?.let { File(workDir, "android").copyRecursively(it, true) }
 
        println("COMPLETE")
    }
 
    @Throws(IOException::class)
    private fun Unit.updateVersion() {
        versionFile.apply {
            if (exists()) {
                delete()
            }
 
            createNewFile()
        }
    }
}

LocalizerPlugin.kt

package localizer
 
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
 
class LocalizerPlugin : Plugin<Project> {
 
    override fun apply(project: Project) {
        project.extensions.create(LOCALIZER_EXTENSION, LocalizerPluginExtension::class.java)
        project.tasks.create("downloadTranslations", DownloadTask::class.java)
 
        File(project.rootDir, ".localizer").mkdir()
    }
}

LocalizerPluginExtension.kt


package localizer
 
const val LOCALIZER_EXTENSION = "localizer"
 
open class LocalizerPluginExtension {
    var apiUrl: String? = null
    var localizationUrl: String? = null
    var module: String? = null
}

build.gradle (buildSrc)

buildscript {
    ext.kotlin_version = '1.3.21'
 
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
 
repositories {
    jcenter()
    google()
}
 
apply plugin: 'kotlin'
 
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
}

Paste import localizer.LocalizerPlugin in your project level build.gradle at top and under the allprojects{} section paste the next code:

build.gradle (project)

apply plugin: LocalizerPlugin
 
localizer {
    apiUrl = "https://localizer.wanari.net/localizer"
    localizationUrl = "https://docs.google.com/spreadsheets/d/totallyRealSpreadsheetIdentifier"
    module = "app"

Now you can run the command below in the terminal which is going to create your corresponding strings.xml files in the app module.

./gradlew downloadTranslations

Angular

Long gone the days where you had to open the notorious i18n folder of @ngx-translate and scroll through the endless json files to correct a few mistakes here and there. All you need is to create a localize.js file in your angular project folder with the following content:

localize.js

const fs = require('fs-extra');
const request = require('request');
const AdmZip = require('adm-zip');
const server = 'https://localizer.wanari.net/localizer?platform=angular&url=';
const url = process.argv[2];
 
request(server + url)
  .pipe(fs.createWriteStream('temp.zip'))
  .on('close', () => {
    try {
      const zip = new AdmZip('./temp.zip');
      zip.extractAllTo('./temp/', true);
      fs.move('./temp/angular', './src/assets', { overwrite: true }, () => {
        fs.removeSync('./temp.zip');
        fs.removeSync('./temp');
        console.log('Localized!');
      });
    } catch (error) {
      console.log('The given URL is incorrect.');
    }
  });

and add the following in your package.json file:

Now just type in the console “npm run localize” and that’s it, your localization files or ready to go.

iOS

The most straightforward way to utilize this tool in Xcode projects is to add a Run Script phase to your build process.

Example script that uses ‘curl’ to call the service, manages the files’ correct placement, then cleans up after itself:

Example iOS script

SHEET_URL="https://docs.google.com/spreadsheets/d/totallyRealSpreadsheetIdentifier"
LOC_FOLDER="${SRCROOT}/Path/To/Loc/Files"
LOC_PATH="$LOC_FOLDER/Localizable.strings"
 
echo "Downloading localized string resources"
curl "https://localizer.wanari.net/localizer?platform=ios&url=$SHEET_URL" --output loc.zip
unzip loc.zip -d loc_files
 
echo "Overwriting current localization"
mv loc_files/iOS/en.lproj/Localizable.strings $LOC_PATH_EN
 
echo "Deleting download remnants"
rm -R loc_files
rm loc.zip

We suggest creating a shell script file and adding it to your project folder, and call the script from Xcode. This makes maintenance a lot easier and also prevents your Xcode project from getting too cluttered.

For example: text If you are not comfortable with your project updating itself every time you build it, one solution would be to add a new Scheme to the project, which executes the script as a Pre-Build Action, and only use this scheme when you actually want to update your loc files.

One important note about this, is that pre-build scripts have a different run folder than the “Run Script” phases, and thus the script’s execution path has to be adjusted as well. The script itself should work the same way though. text Of course it’s completely up to you how you wish to optimize the script or the run conditions, these are just examples, we hope they were useful.

Spring

Just like on every other supported platform, there is an easy and fast way to utilize our localizer tool. You can create a maven profile to download and put the translations into your target, before your project is packaged with just two easy step.

To do that you need to modify your pom.xml just like the example bellow.

pom.xml

<profile>
    <id>translations</id>
    <pluginRepositories>
        <pluginRepository>
            <id>sonatype-public-repository</id>
            <url>https://oss.sonatype.org/content/groups/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
 
    <build>
        <plugins>
            <plugin>
                <groupId>com.googlecode.maven-download-plugin</groupId>
                <artifactId>download-maven-plugin</artifactId>
                <version>1.4.1</version>
                <executions>
                    <execution>
                        <id>download-translations</id>
                        <phase>package</phase>
                        <goals>
                            <goal>wget</goal>
                        </goals>
                        <configuration>
                            <url><![CDATA[http://localizer.wanari.net/localizer?platform=spring&url=$SHEET_URL]]></url>
                            <unpack>true</unpack>
                            <outputDirectory>${project.basedir}/target/translations/</outputDirectory>
                            <outputFileName>translations-{maven.build.timestamp}.zip</outputFileName>
                            <overwrite>true</overwrite>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>${maven-resources-plugin.version}</version>
                <executions>
                    <execution>
                        <id>copy-translations</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
 
                        <configuration>
                            <outputDirectory>${project.basedir}/target/classes/i18n/</outputDirectory>
                            <overwrite>true</overwrite>
                            <resources>
                                <resource>
                                    <directory>${project.basedir}/target/translations/spring/messages</directory>
                                    <includes>
                                        <include>*.properties</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

Terms of use

This tool is powered by Wanari, it is free of charge. No human will look at your data, it is handled by an algorithm. We do not store or use your data for anthing else, but to create the zip file, then to us it will be gone forever. Use this tool at your own risk, Wanari is not responsible for any losses or damages caused by using this tool.

member photo

WE BUILD SOFTWARE WE WOULD USE. And we are picky.

Latest post by @TeamWanari

Hello world!

portrait
union

Do you have a software development problem?

Send an e-mail to hello@wanari.com or give us a call 00-36-20-248-1156

Get in touch with us!