
Tool for importing localized strings from external sources to various project formats, including XML, JSON, and INI files. Supports multiple platforms and integrates with Google Sheets for streamlined localization management.
LocoLaser is a Gradle plugin that syncs localization strings between a Source resource and a Platform resource:
Resource (as Source) ──→ LocoLaser ──→ Resource (as Platform)
Source and Platform are roles, not fixed types. Any resource can act as either. Google Sheets is typically used as Source; file-based resources (Android, iOS, JSON, etc.) can be both.
| Resource | Module | Typical role | README |
|---|---|---|---|
| Google Sheets | resource-googlesheet |
Source | → |
Android strings.xml
|
resource-mobile |
Platform / Source | → |
iOS Localizable.strings
|
resource-mobile |
Platform / Source | → |
| iOS Swift / ObjC codegen | resource-mobile |
Platform | → |
| Kotlin Multiplatform codegen | resource-kotlin-mpp |
Platform | → |
| JSON (i18next) | resource-json |
Platform / Source | → |
GetText .po
|
resource-gettext |
Platform / Source | → |
| INI files | resource-ini |
Platform / Source | → |
| Java Properties | resource-properties |
Platform / Source | → |
See also: plugin/README.md
plugin-all includes the plugin and all resource modules in one dependency.
build.gradle.kts:
buildscript {
repositories {
mavenCentral()
maven("https://plugins.gradle.org/m2/")
}
dependencies {
classpath("ru.pocketbyte.locolaser:plugin-all:2.7.0")
// or: classpath("ru.pocketbyte.locolaser:plugin-kmp:2.7.0")
}
}
apply(plugin = "ru.pocketbyte.locolaser.all")
// or: apply(plugin = "ru.pocketbyte.locolaser.kmp")build.gradle:
buildscript {
repositories {
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "ru.pocketbyte.locolaser:plugin-all:2.7.0"
}
}
apply plugin: "ru.pocketbyte.locolaser.all"Bundle contents:
plugin-all — all resources: mobile, kotlin-mpp, json, gettext, ini, properties, googlesheetplugin-kmp — mobile, kotlin-mpp, json, properties (no gettext, ini, googlesheet)build.gradle.kts:
plugins {
id("ru.pocketbyte.locolaser").version("2.7.0")
}
buildscript {
repositories { mavenCentral() }
dependencies {
classpath("ru.pocketbyte.locolaser:resource-mobile:2.7.0")
classpath("ru.pocketbyte.locolaser:resource-googlesheet:2.7.0")
}
}Add this import when using build.gradle.kts:
import ru.pocketbyte.locolaser.*localize {
config { // unnamed → tasks: localize, localizeForce, localizeExportNew
locales = setOf("base", "en", "de") // required; "base" = default/fallback locale
source { /* ... */ } // required
platform { /* ... */ } // required
conflictStrategy = Config.ConflictStrategy.KEEP_NEW_PLATFORM
trimUnsupportedQuantities = true
}
config("KMP") { // named → tasks: localizeKMP, localizeKMPForce, localizeKMPExportNew
locales = setOf("base", "en", "de")
dependsOnCompileTasks() // call when KMP codegen must precede compilation
source { /* ... */ }
platform { /* ... */ }
}
}| Strategy | Behavior |
|---|---|
REMOVE_PLATFORM |
Source wins; platform strings absent from source are deleted. |
KEEP_NEW_PLATFORM |
Source wins, but platform strings not in source are kept. |
KEEP_PLATFORM |
Platform always wins — source is never overwritten. |
EXPORT_NEW_PLATFORM |
New platform strings (not in source) are pushed back to source. |
EXPORT_PLATFORM |
All platform strings are pushed to source. |
./gradlew localize # sync (Gradle task cache applies)
./gradlew localizeForce # sync, bypass cache
./gradlew localizeExportNew # sync + export new platform strings to source
For named configs, tasks are prefixed with the config name: localizeKMP, localizeKMPForce, etc.
Sync strings from Google Sheets to strings.xml:
build.gradle.kts:
import ru.pocketbyte.locolaser.*
localize {
config {
locales = setOf("base", "en", "de")
source {
googleSheet {
id = "YOUR_SHEET_ID"
keyColumn = "key"
quantityColumn = "quantity"
credentialFile = "./service_account.json"
}
}
platform {
android {
resourcesDir = "./app/src/main/res/"
}
}
}
}Two-config setup — first populate platform string files, then generate the KMP repository:
build.gradle.kts:
import ru.pocketbyte.locolaser.*
localize {
// Step 1: sync strings from Google Sheets to Android/iOS resource files
config("Mobile") {
locales = setOf("base", "en", "de")
source {
googleSheet {
id = "YOUR_SHEET_ID"
keyColumn = "key"
credentialFile = "./service_account.json"
}
}
platform {
android { resourcesDir = "./android/src/main/res/" }
ios { resourcesDir = "./ios/" }
}
}
// Step 2: generate KMP repository code from the Android resource files
config("KMP") {
locales = setOf("base", "en", "de")
dependsOnCompileTasks()
source {
android { resourcesDir = "./android/src/main/res/" }
}
platform {
kotlinMultiplatform(project) {
srcDir = "./shared/build/generated/locolaser/"
repositoryInterface = "com.example.StringRepository"
repositoryClass = "com.example.StringRepositoryImpl"
android()
ios()
}
}
}
}The generated KMP code requires a runtime dependency in your shared module:
build.gradle.kts (shared module):
kotlin {
sourceSets {
commonMain.dependencies {
implementation("ru.pocketbyte.locolaser:runtime:2.7.0")
}
}
}credentialFile in your config.Without credentialFile, LocoLaser falls back to interactive OAuth — only works on developer machines with a browser.
| key | base | en | de | quantity |
|---|---|---|---|---|
| app_title | App | App | App | |
| file_count | %1$d files | %1$d files | %1$d Dateien | other |
| file_count | %1$d file | %1$d file | %1$d Datei | one |
base column → default/fallback localequantity column and repeat rows to support plural formsworkDir, which defaults to the module directory.localize is cached by Gradle. Use localizeForce for a guaranteed fresh run.See Migration guide for upgrading from earlier versions.
Copyright © 2017 Denis Shurygin. All rights reserved.
Contacts: <mail@pocketbyte.ru>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
LocoLaser is a Gradle plugin that syncs localization strings between a Source resource and a Platform resource:
Resource (as Source) ──→ LocoLaser ──→ Resource (as Platform)
Source and Platform are roles, not fixed types. Any resource can act as either. Google Sheets is typically used as Source; file-based resources (Android, iOS, JSON, etc.) can be both.
| Resource | Module | Typical role | README |
|---|---|---|---|
| Google Sheets | resource-googlesheet |
Source | → |
Android strings.xml
|
resource-mobile |
Platform / Source | → |
iOS Localizable.strings
|
resource-mobile |
Platform / Source | → |
| iOS Swift / ObjC codegen | resource-mobile |
Platform | → |
| Kotlin Multiplatform codegen | resource-kotlin-mpp |
Platform | → |
| JSON (i18next) | resource-json |
Platform / Source | → |
GetText .po
|
resource-gettext |
Platform / Source | → |
| INI files | resource-ini |
Platform / Source | → |
| Java Properties | resource-properties |
Platform / Source | → |
See also: plugin/README.md
plugin-all includes the plugin and all resource modules in one dependency.
build.gradle.kts:
buildscript {
repositories {
mavenCentral()
maven("https://plugins.gradle.org/m2/")
}
dependencies {
classpath("ru.pocketbyte.locolaser:plugin-all:2.7.0")
// or: classpath("ru.pocketbyte.locolaser:plugin-kmp:2.7.0")
}
}
apply(plugin = "ru.pocketbyte.locolaser.all")
// or: apply(plugin = "ru.pocketbyte.locolaser.kmp")build.gradle:
buildscript {
repositories {
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "ru.pocketbyte.locolaser:plugin-all:2.7.0"
}
}
apply plugin: "ru.pocketbyte.locolaser.all"Bundle contents:
plugin-all — all resources: mobile, kotlin-mpp, json, gettext, ini, properties, googlesheetplugin-kmp — mobile, kotlin-mpp, json, properties (no gettext, ini, googlesheet)build.gradle.kts:
plugins {
id("ru.pocketbyte.locolaser").version("2.7.0")
}
buildscript {
repositories { mavenCentral() }
dependencies {
classpath("ru.pocketbyte.locolaser:resource-mobile:2.7.0")
classpath("ru.pocketbyte.locolaser:resource-googlesheet:2.7.0")
}
}Add this import when using build.gradle.kts:
import ru.pocketbyte.locolaser.*localize {
config { // unnamed → tasks: localize, localizeForce, localizeExportNew
locales = setOf("base", "en", "de") // required; "base" = default/fallback locale
source { /* ... */ } // required
platform { /* ... */ } // required
conflictStrategy = Config.ConflictStrategy.KEEP_NEW_PLATFORM
trimUnsupportedQuantities = true
}
config("KMP") { // named → tasks: localizeKMP, localizeKMPForce, localizeKMPExportNew
locales = setOf("base", "en", "de")
dependsOnCompileTasks() // call when KMP codegen must precede compilation
source { /* ... */ }
platform { /* ... */ }
}
}| Strategy | Behavior |
|---|---|
REMOVE_PLATFORM |
Source wins; platform strings absent from source are deleted. |
KEEP_NEW_PLATFORM |
Source wins, but platform strings not in source are kept. |
KEEP_PLATFORM |
Platform always wins — source is never overwritten. |
EXPORT_NEW_PLATFORM |
New platform strings (not in source) are pushed back to source. |
EXPORT_PLATFORM |
All platform strings are pushed to source. |
./gradlew localize # sync (Gradle task cache applies)
./gradlew localizeForce # sync, bypass cache
./gradlew localizeExportNew # sync + export new platform strings to source
For named configs, tasks are prefixed with the config name: localizeKMP, localizeKMPForce, etc.
Sync strings from Google Sheets to strings.xml:
build.gradle.kts:
import ru.pocketbyte.locolaser.*
localize {
config {
locales = setOf("base", "en", "de")
source {
googleSheet {
id = "YOUR_SHEET_ID"
keyColumn = "key"
quantityColumn = "quantity"
credentialFile = "./service_account.json"
}
}
platform {
android {
resourcesDir = "./app/src/main/res/"
}
}
}
}Two-config setup — first populate platform string files, then generate the KMP repository:
build.gradle.kts:
import ru.pocketbyte.locolaser.*
localize {
// Step 1: sync strings from Google Sheets to Android/iOS resource files
config("Mobile") {
locales = setOf("base", "en", "de")
source {
googleSheet {
id = "YOUR_SHEET_ID"
keyColumn = "key"
credentialFile = "./service_account.json"
}
}
platform {
android { resourcesDir = "./android/src/main/res/" }
ios { resourcesDir = "./ios/" }
}
}
// Step 2: generate KMP repository code from the Android resource files
config("KMP") {
locales = setOf("base", "en", "de")
dependsOnCompileTasks()
source {
android { resourcesDir = "./android/src/main/res/" }
}
platform {
kotlinMultiplatform(project) {
srcDir = "./shared/build/generated/locolaser/"
repositoryInterface = "com.example.StringRepository"
repositoryClass = "com.example.StringRepositoryImpl"
android()
ios()
}
}
}
}The generated KMP code requires a runtime dependency in your shared module:
build.gradle.kts (shared module):
kotlin {
sourceSets {
commonMain.dependencies {
implementation("ru.pocketbyte.locolaser:runtime:2.7.0")
}
}
}credentialFile in your config.Without credentialFile, LocoLaser falls back to interactive OAuth — only works on developer machines with a browser.
| key | base | en | de | quantity |
|---|---|---|---|---|
| app_title | App | App | App | |
| file_count | %1$d files | %1$d files | %1$d Dateien | other |
| file_count | %1$d file | %1$d file | %1$d Datei | one |
base column → default/fallback localequantity column and repeat rows to support plural formsworkDir, which defaults to the module directory.localize is cached by Gradle. Use localizeForce for a guaranteed fresh run.See Migration guide for upgrading from earlier versions.
Copyright © 2017 Denis Shurygin. All rights reserved.
Contacts: <mail@pocketbyte.ru>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.