LocoLaser: translating applications into Google Sheets
In practice, you often have to release applications immediately on Android and iOS, and sometimes on Windows Phone. In this case, some developers solve the problem of localization in advance, translating each platform separately. Agree, not the best trip. At the time when I decided to solve this problem, on the network it was already possible to find utilities for downloading string resources from Google Sheets, but most of them looked at least clumsy. I was categorically not happy with this alignment. As a result, LocoLaser was born - an easy-to-use, but very smart utility in Java. For a couple of years I used it exclusively in my work projects. During this time, she managed to grow quite rich functionality, and now she is ready to be presented to the public.
In this article I will talk about what LocoLaser is and how to integrate it into your project. First, we’ll take a look at the features of the utility, and then move on to more specific things, such as the Gradle plugin for Android and Bash scripts for iOS. I’m a lazy programmer enough to do the same things from time to time, so everything is done so that after the initial setup, your further work will be reduced only to launching the necessary task when it is needed. For translators, the translation looks very simple, we share the Google table with rows for them and they enter the translation in the appropriate columns. If you need to add a language, just add another column with a new language. Google Sheets allows you to individually configure access to edit the table,
WARNING
Since the publication of this article, the library has undergone some significant changes and some of the points described in this article are no longer relevant. You can find the latest instructions and examples in the repositories listed at the end of this article.
configuration file
But back to the utility. Too simplified, it itself is a regular jar file. You can download it from the Bintray repository . The easiest way to start the utility is to run it from the console. The console command may look like this:
java -jar loco-laser-google.jar localization_config.json
Note # 1
To execute a command, Java must be installed on the machine.
Note No. 2After the command is executed, the corresponding resource files will be created and arranged in the necessary folders. The path to the configuration file should be passed as the first parameter. In the above example, this is a file
Once again I note that you do not need to download jar directly to use the utility in your projects. For these purposes, there are ready-made scripts and plugins.
"localization_config.json"
. It is he who is responsible for setting up all the intricacies of import and export. The file is a text JSON that can look, for example, like this:{
"platform": "android",
"source": {
"id":"1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM",
"column_key":"key",
"column_locales":["base", "ru"]
}
}
Этот пример содержит минимум параметров для запуска. Давайте разберем что же они означают.
Параметр
"platform"
указывает платформу для которой будет произведен импорт. Возможен один из 2-х вариантов: "android"
и "ios"
(Windows Phone пока не поддерживается, но это лишь вопрос времени). Помимо формата, платформа отвечает за расположение файлов с ресурсами, их имена, а также место для складирования временных файлов.Параметр
"source"
— это JSON объект. Его содержимое отвечает за то, откуда и как будут загружены строки. Минимальным набором параметров для него будет: "id"
, "column_key"
и "column_locales"
.В качестве
"source.id"
Google table id must be specified. In order to find this identifier you need to look at the URL to the table. It should look like docs.google.com/spreadsheets/d/1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM . In this example, the identifier will be . By the way, the link is clickable and on it you can see a real example of a table.
With the rest of the parameters, everything is quite simple, it contains the name of the column with keys, and an array of column names with values, where the column name is also a locale. A word is reserved for the base locale ."1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM"
"source.column_key"
"source.column_locales"
"base"
Here it is worth making a small digression and determining the structure of the table and at the same time sorting out what column names are. If it’s simple, then LocoLaser treats the first “not crossed out” line as a line with column names. You ask, “What else is the crossed out line?”. The fact is that the very first column in the table is used for official purposes. At the moment, you can “cross out” the line by adding a character in the first column
"-"
. The crossed out line is completely ignored and does not participate in localization. If you look at the table using the link above, you will see that the first line with the “human” headings is “crossed out” and is only a decorative element of the table. You can also “delete” lines with resources, for example, when a line is not used anywhere, but it is a pity to delete.Complete list of parameters
The above parameters are enough for the utility to work. However, there are others. I will not describe each in detail, but only give a complete list of them in the form of tables.
Source
Parameter | A type | Description |
---|---|---|
id | String | Google Sheet spreadsheet ID. The table URL contains this identifier (https://docs.google.com/spreadsheets/d/*sheet_id*). Required. |
column_key | String | The name of the key column. Required. |
column_locales | Strings array | An array of column names with values, where the column name is also a locale. For the base locale, use the column with the name "base" . Required. |
column_quantity | String | The name of the column with quantitative values. The cells in the table must contain one of the following values: zero , one , two , few , many , other . An empty string is treated as other. Optional parameter. By default, quantitative values are not used. |
column_comment | String | The name of the comment column. Optional parameter. By default, comments are not written to the resource file. |
worksheet_title | String | The name of the sheet in the Google Sheet table. Optional parameter. By default, the first sheet is used. |
credential_file | String | The path to the file containing the credentials for OAuth authentication. When using a relative path, the path to the file is relative to the working directory. Optional parameter. |
type | String | Type of data source. It must be "googlesheet" . Optional parameter. However, if you want to be sure that the configuration file and the utility are working with the same source type, you should specify its type. |
As I said earlier, the platform sets a number of parameters specific to it. These parameters can be changed if you define the platform not as a string, as was done above, but as a JSON object. Here is a list of the possible properties of this object:
Parameter | A type | Description |
---|---|---|
type | String | Type of platform. The possible values are: "android" or "ios" . Required. |
res_name | String | Resource file name without extension. The extension is selected depending on the type of file. Optional parameter. Default Values: Android - "strings" iOS - "Localizable" |
res_dir | String | The path to the resource directory. Optional parameter. Default Values: Android - "./src/main/res/" iOS - "./" |
temp_dir | String | The path to the directory for storing temporary files. Optional parameter. Default Values: Android - "./build/tmp/" iOS - "../DerivedData/LocoLaserTemp/" |
Important!Other parameters. The
All relative paths are relative to the working directory. By default, the configuration file directory is used as the working directory.
configuration may contain several parameters, except for the platform and source. All of them can significantly affect the result:
Parameter | A type | Description |
---|---|---|
work_dir | String | The path to the working folder. All relative paths are relative to this folder. The default is the configuration file directory. |
force_import | Boolean | The utility remembers the state of resources at the last execution and tries not to start importing without unnecessary need. To ignore this and always import in full, set it force_import to equal true . The default value is false. |
conflict_strategy | String | Define a way to resolve conflicts when combining platform resources and resources from Google Sheets. 3 options are possible:
keep_new_platform . |
duplicate_comments | Boolean | A duplicate_comments = false comment will not be added to the resource file if this comment is equal to the localized string. The default value false . |
delay | Long | Time in minutes defining the minimum time until the next localization. Localization will not be performed more often than specified in the delay parameter. If used, the force_import delay is ignored. |
This parameter is
duplicate_comments
especially useful if you use the base locale as comments. In this case, in localized resources there will always be a comment with text in the base language (usually English), and comments in the base resource will be absent, since in this case they repeat the values of the string and are superfluous.Example of Android resources with duplicate_comments = false
/values/strings.xml
LocoLaser example This is example application of how to use the LocoLaser. Plural string examples
/values-ru/strings.xml
/* LocoLaser example */
LocoLaser пример
/* This is example application of how to use the LocoLaser. */
Это пример приложения по использованию LocoLaser.
/* Plural string examples */
Примеры Plural строк
Customized localization_config.json configuration example
{
"platform": {
"type":"android",
"res_name":"strings_intro"
},
"source": {
"type":"googlesheet",
"id":"1JZxUcu30BjxLwHg12bdHTxjDgsGFX9HA9zC4Jd8cuUM",
"column_key":"key",
"column_locales":["base", "ru"],
"column_comment":"base",
“worksheet_title”:”Strings intro”
},
"force_import":true,
"conflict_strategy":”keep_new_platform”,
"delay":60,
}
Console arguments
When you start the localizer from the console, you can change some configuration parameters. To do this, add the appropriate arguments. Important: the path to the configuration file is always specified first.
Argument | Description |
---|---|
--force or --f | Flag. Sets"force_import = true" |
-cs *string conflict strategy* | Overrides property "conflict_strategy" |
-delay *long delay* | Overrides property "delay" |
Command example
java -jar loco-laser-google.jar localization_config.json --f -cs keep_new_platform
Android and Gradle Plugin
When working with LocoLaser in Android, you should use a special Gradle plugin. He adds several tasks to the project, united in the “localization” group . There are 2 options for connecting the plugin, "classic" and "alternative."
The classic way to connect the plugin
Open the
After that, open
"build.gradle"
root project file and add the following lines:buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.ru.pocketbyte.locolaser:plugin:1.0.1"
}
}
After that, open
"build.gradle"
the application module file and add one line to the beginning of the file:apply plugin: "ru.pocketbyte.locolaser"
An alternative way to connect a plugin
This method is suitable for projects with Gradle version higher than 2.1 and uses the so-called “incubating” functionality (use it at your own risk, the Gradle team warns us). For him, you do not need to register anything in the root
Once again, I note the string must be in the VERY top. Nothing else should be in front of her. Otherwise, the project is not even synchronized.
"build.gradle"
. Open the "build.gradle"
module you need and at the very top of the file add the following lineplugins { id "ru.pocketbyte.locolaser" version "1.0.1" }
Once again, I note the string must be in the VERY top. Nothing else should be in front of her. Otherwise, the project is not even synchronized.
After connecting the plugin, in one of the above ways, in the same file, add the following dependency:
dependencies {
...
localize 'ru.pocketbyte.locolaser:locolaser-mobile-googlesheet:1.1.+'
}
Now it remains only to put the configuration file in the folder of the application module and you can start working. By default, as a configuration file, the plugin uses a file with a name
"localize_config.json"
. After the project is synchronized, the “localization” group will appear in the Gradle list of tasks , it contains 3 tasks. localize - Starts LocoLaser with standard parameters;
localizeForce - Launches LocoLaser with a flag
"--force"
; localizeExportNew - Starts LocoLaser with the
"--force"
and flag "conflict_strategy" = "export_new_platform"
. If you want localization to start on every build, add a dependency to the preBuild task :
afterEvaluate {
preBuild.dependsOn project.tasks.localize
}
An example of an Android project on GitHub: github.com/PocketByte/locolaser-android-example
Using the Gradle plugin. Localization is built into the build process.
Localization of iOS applications
Unlike Android, iOS does not have such a powerful build system as Gradle, so you have to use workarounds, since there are plenty of them and they fit perfectly with iOS ideology. I suggest storing the configuration file in the source folder, usually the name of this folder matches the name of the project (this is the only location where you can use the default values).
So, you created the configuration file, and put it in the source folder. How to use it? Since the artifacts of the utility are located on the remote maven repository, I went the way of writing a bash script that simulates a dependency management system. There he is:
localize.command
GROUP="ru/pocketbyte/locolaser"
ARTIFACT="locolaser-mobile-googlesheet"
VERSION="1.1.1"
CONFIG_FILE="localization_config.json"
cd "`dirname \"$0\"`"
ARTIFACTS_DIR="../DerivedData/LocoLaserTemp/artifacts/$GROUP/"
mkdir -p $ARTIFACTS_DIR
ARTIFACT_FILE="$ARTIFACTS_DIR$ARTIFACT-$VERSION.jar"
if [ -f $ARTIFACT_FILE ]
then
echo "Artifact already downloaded"
else
ARTIFACT_URL="https://bintray.com/pocketbyte/maven/download_file?file_path=$GROUP/$ARTIFACT/$VERSION/$ARTIFACT-$VERSION.jar"
echo "Loading: $ARTIFACT_URL"
curl -L -o $ARTIFACT_FILE $ARTIFACT_URL
if [ $? -eq 0 ]
then
echo "Artifact downloaded"
else
exit $?
fi
fi
java -jar $ARTIFACT_FILE $CONFIG_FILE
This script downloads the jar file from the remote maven repository and places it in a folder
"../DerivedData/LocoLaserTemp/artifacts/"
, after which it launches for execution. As a configuration file, a file with a name is used "localize_config.json"
, as in Android. The decision is quite rude, you have to accurately indicate the version of the artifact used. However, it copes with its task perfectly and does not need to store the utility file in the jar project. As you can guess from the relative paths, the script should be saved in the same folder as the configuration file. I have several such scripts in my project: localize.command , localizeForce.command and localizeExportNew.command . All these commands repeat the tasks from the Gradle plugin above.If you want localization to be performed on every build, add the appropriate Run scrip :
Run script should go immediately after the Target Dependencies phase . This is important, because if new lines are imported, for example, after the Copy Bundle Resources phase , you will receive the desired result late, only in the next build.
An example of an iOS project on GitHub: github.com/PocketByte/locolaser-ios-example
Uses a bash script. Localization is built into the build process.
Windows Phone and other platforms
Unfortunately, I have absolutely no experience in developing for this platform, and in the organization in which I currently work, the development of Windows Phone applications has not been practiced for a long time. I sincerely apologize for posting the article in the Windows Phone hub. However, I did not find a better way to get the attention of Windows Phone developers. So if you are a Windows Phone developer with extensive experience, and you have something to tell me about this topic, please contact me. Also, if one of the readers has an idea about using LocoLaser in other platforms or services, do not hesitate to write, I am open to suggestions.
Credential file
To access Google spreadsheets, you must go through OAuth authorization. If the parameter is not specified in the configuration file
"source.credential_file"
, when the localizer starts, the browser will automatically open with the Google authentication page. After successful authorization, the utility will continue its work. The next time you do not need to log in to this table. Since the above approach requires user intervention, it is not suitable for situations where the localization process should take place automatically. In order for the process to always go automatically, you must specify
credential_file
. As a credential file, the Google Service Account file is suitable. To create it in the Google Developers console, you need to do the following:- Create a new project if it has not yet been created for your application.
- Open the list of Service accounts . If you are prompted to select a project, select the project of your application.
- Click Create a service account .
- In the Create Service Account window , enter the name of the new service account, enable the Create a new private key flag , select the JSON key type. The account role does not need to be selected. Click Create .
A service account will be created, after which the private key file will be downloaded automatically. The path to this file should be specified as a parameter
"source.credential_file"
. In order for the service account to have access to the Google spreadsheet, it must be shared for the previously created service account. When sharing, specify the Service account identifier as Email. This identifier can be seen in the list of service accounts, or peek in the downloaded key file.Example service key file
{
"type": "service_account",
"project_id": "myapp-1086",
"private_key_id": "b67c2edcc47c7053c035d8681c8eb7e9f4d90c09",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUV49mSQB2NSO\nw+tfQWq4pP63U4t7W8V6O6E7FABbYS5N4g35nRzVEj5NciqI27shHSKVrsl7U5ji\nM0IIA+vi+dgHXwHfCPhS9d85xZ73fuqFaj29iru+pTq9tuNieLDl60L04oCc1qQKBgQDLsMMnX2r9qkQw01H5L2WRB9R6er+bO4DE\n8Ecpripfd/e7qq89WcGu1H8S+3Jy49dBmN709vPvVcsGQx8mDdYdm4P0WPkKbgTo\nt12OA07uiY6Zn54rW5CXrjdoscPXB94AS2ps4M3/xY5hHTxwtS8yJxoUgTVEfgNB\nDwzNrZSCVEMCoBAIYl6rWGITgNaR0+FLuP+kjZw\nHsLdkU8173J3nhuYhxo7N90BhO08lquIQ7bJAoGBAKLz\nVRxRdFlcdlMNK34K0dkVh4E4Y8K+9oQWqQeKIrHfWpuSr8CH5q+Dpek8qVGKPnFm\n567XRUzJuLLYzbl2xj1HZWf8KbeTTnALKYg8Jz\nxXXvLlZl2OJ8Frr9ry1DEszPkwWwTQJg5bRG7Z//QfpyEZ2PUvpCNVVpeRuMmUhv\nu5rSLa0G+C97/XIGz/O/1ME9WXU6ZNRwwDkSDw6L7AIrXY8V+8pIRL9e0ks4Uw/A\n6ACYrlYMYYAIl79MNrUrizvF5KwxLiohHJ5KVpThGuRZDaidCPp9BL/h8tfhXPel\nwQot9dM8P4CmQNR/fMpytQSVk7vv95B2JHrt6QmIsQKBgQD9BJ8gfZUVhlxtuaWO\nMKl1PjiD+YKhz4rmIZUKM84xphsGYUBhH29s1zb98u9vlEnlx3bGUtDakNnjTDQp\nagQv22+6STL+0s1haOxyfbi1jIzXvzh47yij6+v7WEIdNj45WV9kpcFTi2oUXURt\nEi3WskYTijYGbDNQmpG2kmY\nDz0KdeTFJxsnFstTT/VozEGvNIHf+8PhKv0123dBFuqSgBD5SFHDp3tQ2IzC81Jm\n5FJLldk3Hw1QRh6+WiEJBTX6nFU4DB5tVXKhbPOvhqYwI/CUYWWbVBQCQgqURcyr\nUdRxAHyDrxKhNrmXXKAEwR0rDz2uGTQCfJ0Zyk/Z1E7iDl/SDfYSSD70wAgGblH2\nAAIQzeoPAgMBAAECggEAb7Trswhft3qmb1V9LEzzN+OtxvHfqqKAkFO4Ijz1+b6R\n3/t4P7KTRhOqaHTZ7zjlu/kbsKzc9casRY+lqybp4/c4jNaGBklG6Vmu96E9wKBgEFPsRe88v/UaAV213Jlw0hdYE9I19yW6z7OSl+Q0dflDbLO5cRs\nTeHlh+9zhzThLVYf79vvwrO4klXm9Mv/7sa/uQ54GK7IkXVklSxUoZpThoOme5hT\n+8ScgJSnyqEpwFQjaslbNBUxtpc9IA2bseP1S7aCVDfZtEp7rHqOFZTvSgol5YD/AoGAdIMNKvg0IiS2xcIEbyHa\nPTNrUfpHeJk/P+Frr1cmDHhGe1l0FWNg9EDlhItAW5EP15ubZPdWQRrV7RuydJlc\ngWesc2RLPIG8+so1TpG1F62+tsQ9lGSF5imiew1x7sQ3H0VIpGtSfTvSMep6fuE7\niuSVbY0UnpxGnqzo9TBAYS4=\n-----END PRIVATE KEY-----\n",
"client_email": "localizator@myapp-1086.iam.gserviceaccount.com",
"client_id": "704739729071909788554",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/localizator%40myapp-1086.iam.gserviceaccount.com"
}
"client_email"
- this is the account identifier.Conclusion
Once again, I note that the utility architecture is designed in such a way that it can be easily expanded or supplemented, the source and platform work independently. For example, if you wish, you can supplement the list of sources and use a database or some Web service that has a suitable API for this as source. So the light didn’t converge here on Google Sheets, and advocates of “anti-googling” will be able to find a suitable solution for themselves.
I would also like to thank Inna (Foenix) for her help in writing this article. Without it, the article would not be so intelligible and understandable. Inna, thank you for your advice and comments.
On this I will end. Thank you for taking the time to familiarize yourself with my creation. I will be glad to get your opinion, suggestions or criticism in the comments. And do not forget to press the up or down arrows, it is very important for me, I really want to know how the Habr community accepted my work.
Links to the source:
→ Source Code on GitHub LocoLaser
→ Source Code Gradle plugin on GitHub
→ Example for All Android
→ Example for iOS
→ Link to Bintray
→ Table Example