Skip to content

Commit

Permalink
Introduce JS output-type + collections
Browse files Browse the repository at this point in the history
  • Loading branch information
LcTwisk committed Jul 27, 2020
1 parent 1b636e1 commit cacd8a8
Show file tree
Hide file tree
Showing 24 changed files with 747 additions and 771 deletions.
14 changes: 14 additions & 0 deletions MIGRATION.md
@@ -0,0 +1,14 @@
# Localicious Migration

### From 0.9.x to 1.0.x

Previously, we only allowed fixed groups (IOS|ANDROID|SHARED) in the Localicipe root. By introducing `collections` in Localicious v1.0.0, we're giving the user more control by letting them define their own groups. Collections can have any name and can be rendered in any output-type (`android`|`ios`|`js`).

This is a breaking change, which means that existing commands need to be updated.
Previously, all Android strings could be rendered using:
`localicious render ./copy.yaml ./output_path --platforms android --languages en`

Using Localicious v1.0.x, the same result can be achieved by changing the command to:
`localicious render ./copy.yaml ./output_path --outputTypes android --languages en --collections ANDROID,SHARED`

As you can see, `platforms` is renamed to `outputTypes`, and the new parameter `collections` is provided to specifiy which collections should be included in the output.
74 changes: 62 additions & 12 deletions README.md
Expand Up @@ -4,7 +4,7 @@ localicious is a toolchain for working with localization files in a platform-agn

* Maintain all your localized copy and accessibility key/value pairs in one file, grouped per component.
* Verify the integrity of your base localization file against a schema
* Generate locale files for both Android and iOS from your base localization file
* Generate locale files for both Android, iOS or JS from your base localization file

The goals of localicious are:

Expand Down Expand Up @@ -35,10 +35,17 @@ localicious requires node 10.12.0 or later.

## The Localicipe

The central concept of localicious is the so-called Localicipe. It is a YAML file that contains all localized copy and accessibility strings grouped by platform, feature and screen:
The central concept of localicious is the so-called Localicipe. It is a YAML file that contains all localized copy and accessibility strings grouped by feature and screen. The strings in the Localicipe can be divided into different collections. Multiple collections can be combined when [Converting the Localicipe](#converting-the-localicipe) into platform specific outputs.

Using collections it's easy to keep track of strings that are used on a single platform and strings that are shared across multiple platforms.
For an existing iOS and Android app, it could be useful to create three different collections:
- `IOS`(containing all iOS specific strings)
- `ANDROID`(containing all Android specific strings)
- `SHARED`(containing all strings that are shared between iOS and Android).

Each leaf node in a collection is either a `COPY` group or an `ACCESSIBILITY` group. The required structure of both groups is explained below:
```
IOS|ANDROID|SHARED:
<COLLECTION NAME>:
Feature:
Screen:
Element:
Expand Down Expand Up @@ -72,15 +79,35 @@ source:
languages:
- en
- nl
platforms:
outputTypes:
- IOS
collections:
- IOS
- ANDROID
- SHARED
```

To retrieve the latest version of the file in your repository, simply run `localicious install`. localicious also supports specifying a specific Git branch (by adding `:branch`).

## Converting the Localicipe

Using the `render` command, a Localicipe can be converted into platform specific outputs. Here's an overview on how the command works:

**Syntax**

`localicious render <localicipe path> <output path>`

**Options**

`--outputTypes/-ot` (required)
- The platform/language for which the output files will be generated (`Localized.strings` for iOS, `strings.xml` for Android, `strings.json` for JS).
- Available options are: `ios`, `android` or `js`

`--collections/-c` (required)
- The collections, defined in the Localicipe, that should be included into the output.

`--languages/-l` (required)
- The languages that should be included into the output.

Consider the following Localicipe:

```
Expand Down Expand Up @@ -124,7 +151,7 @@ SHARED:

By running the following localicious command:

`localicious render ./copy.yaml ./output_path --platforms android --languages en`
`localicious render ./copy.yaml ./output_path --outputTypes android --collections ANDROID,SHARED --languages en`

We can generate a strings.xml file for Android with the English translations provided:

Expand All @@ -143,11 +170,11 @@ We can generate a strings.xml file for Android with the English translations pro

A similar file with the Dutch translations will be created as well if we request localicious to do so:

`localicious render ./copy.yaml ./output_path --platforms android --languages en,nl`
`localicious render ./copy.yaml ./output_path --outputTypes android --collections ANDROID,SHARED --languages en,nl`

By changing the destination, like so:
By changing the destination output type, like so:

`localicious render ./copy.yaml ./output_path --platforms ios --languages en`
`localicious render ./copy.yaml ./output_path --outputTypes ios --collections IOS,SHARED --languages en`

the following Localizable.strings file will be generated for iOS:

Expand All @@ -161,7 +188,26 @@ the following Localizable.strings file will be generated for iOS:

## Validating

Whenever we make changes to the Localicipe, it is important to verify that the format of the file is still correct. Imagine that we change the file in the previous example and add another entry for iOS:
Whenever we make changes to the Localicipe, it is important to verify that the format of the file is still correct.
Using the `validate` command, a Localicipe can be validated.

**Syntax**

`localicious validate <localicipe path> <output path>`

**Options**

`--collections/-c` (required)
- The collections, defined in the Localicipe, that should be validated.

`--required-languages/-l` (required)
- The languages that are required in the provided Localicipe.

`--optional-languages/-o`
- The languages that are optional in the provided Localicipe.


Imagine that we change the file in the previous example and add another entry for iOS:

```
Settings:
Expand All @@ -173,7 +219,7 @@ Settings:

Using the validation feature, we can validate whether the structure of the file is still correct after the change:

`localicious validate ./copy.yaml --required-languages en,nl`
`localicious validate ./copy.yaml --collections IOS --required-languages en,nl`

Since we forgot to add a Dutch localization for the `Settings.PushPermissionsRequest.Subtitle.COPY` key, this will fail:

Expand All @@ -183,6 +229,10 @@ Since we forgot to add a Dutch localization for the `Settings.PushPermissionsReq

localicious also supports the concept of optional languages. If we were to run the validator as follows:

`localicious validate ./copy.yaml --required-languages en --optional-languages nl`
`localicious validate ./copy.yaml --collections IOS --required-languages en --optional-languages nl`

the above file would pass validation even without the Dutch translation missing for some entries.

## Migration

Read all migration details in our [Migration Guide](MIGRATION.md).
11 changes: 9 additions & 2 deletions bin/localicious-install
Expand Up @@ -23,14 +23,21 @@ loadYaml(localiciousConfig).onSuccess(localiciousConfig => {
path,
options: {
languages: localiciousConfig.languages,
platforms: localiciousConfig.platforms
outputTypes: localiciousConfig.outputTypes,
collections: localiciousConfig.collections
}
};
})
.flatMap(configuration => {
const { path, options } = configuration;

return validateAndRender(path, outputPath, options.languages, options.platforms);
return validateAndRender(
path,
outputPath,
options.languages,
options.outputTypes,
options.collections
);
})
.onSuccess(() => {
console.log(`✅ Your Localicipe was rendered successfully.`);
Expand Down
95 changes: 61 additions & 34 deletions bin/localicious-render
@@ -1,59 +1,86 @@
#!/usr/bin/env node
const path = require("path");
const program = require("commander");
const keywords = require("../src/model/keywords");
const validateAndRender = require("../src/actions/validateAndRender");

const commaSeparatedList = value => {
return value.split(",");
};

program
.option("-l --languages <languages>", "output languages", commaSeparatedList)
.option("-p --platforms <platforms>", "target platforms", commaSeparatedList)
.parse(process.argv);
const hasValidLanguages = languages => {
if (languages === undefined || languages.length == 0) {
console.error(`❌ This command requires one or more output languages.`);
return false;
}
return true;
};

if (program.args.length < 2) {
console.log(`Usage: localicious render <localicipe> <output_path> -l <languages> -p <platforms>`);
process.exit(1);
}
const hasValidCollections = collections => {
if (collections === undefined || collections.length == 0) {
console.error(`❌ This command requires one or more string collections.`);
return false;
}
return true;
};

const localicipePath = path.resolve(process.cwd(), program.args[0]);
const outputPath = path.resolve(process.cwd(), program.args[1]);
const hasValidOutputTypes = outputTypes => {
const supportedOutputTypes = Object.keys(keywords.outputType).map(s => s.toLowerCase());

if (program.languages === undefined || program.languages.length == 0) {
console.error(`This command requires one or more output languages.`);
process.exit(1);
}
if (outputTypes === undefined || outputTypes.length == 0) {
console.error(
`❌ This command requires one or more output types. Supported output types: '${supportedOutputTypes.join(
" | "
)}'.`
);
return false;
}

const supportedPlatforms = ["ios", "android"];
if (program.platforms === undefined || program.platforms.length == 0) {
console.error(
`This command requires one or more target platforms. Supported target platforms: '${supportedPlatforms.join(
" | "
)}'.`
);
process.exit(1);
}
const invalidOutputTypes = outputTypes.filter(option => !supportedOutputTypes.includes(option));

const invalidPlatforms = program.platforms.filter(option => {
return !supportedPlatforms.includes(option);
});
if (invalidOutputTypes.length != 0) {
console.error(
`❌ Received invalid output types: '${invalidOutputTypes.join(
" | "
)}'. Supported output types: '${supportedOutputTypes.join(" | ")}'.`
);
return false;
}
return true;
};

if (invalidPlatforms.length != 0) {
console.error(
`Received invalid target platforms: '${invalidPlatforms.join(
" | "
)}'. Supported target platforms: '${supportedPlatforms.join(" | ")}'.`
program
.option("-l --languages <languages>", "output languages", commaSeparatedList)
.option("-ot --outputTypes <output_types>", "output types", commaSeparatedList)
.option("-c --collections <collections>", "translation collections", commaSeparatedList)
.parse(process.argv);

if (
program.args.length < 2 ||
!hasValidLanguages(program.languages) ||
!hasValidOutputTypes(program.outputTypes) ||
!hasValidCollections(program.collections)
) {
console.log(
`Usage: localicious render <localicipe> <output_path> -l <languages> -ot <output_types> -c <collections>`
);
process.exit(1);
}

const platforms = program.platforms.map(platform => platform.toUpperCase());
const localicipePath = path.resolve(process.cwd(), program.args[0]);
const outputPath = path.resolve(process.cwd(), program.args[1]);
const outputTypes = program.outputTypes.map(outputType => outputType.toUpperCase());
const collections = program.collections.map(collection => collection.toUpperCase());

validateAndRender(localicipePath, outputPath, program.languages, platforms)
validateAndRender(localicipePath, outputPath, program.languages, outputTypes, collections)
.onSuccess(() => {
console.log(
`✅ Your Localicipe has been rendered successfully for platforms: ${program.platforms} and languages: ${program.languages}.`
`
✅ Your Localicipe has been rendered successfully for:
- outputTypes: ${program.outputTypes}
- languages: ${program.languages}
- collections: ${program.collections}
`
);
process.exit(0);
})
Expand Down
10 changes: 8 additions & 2 deletions bin/localicious-validate
Expand Up @@ -11,11 +11,12 @@ const commaSeparatedList = value => {
program
.option("-r --required-languages <languages>", "required languages", commaSeparatedList)
.option("-o --optional-languages <languages>", "optional languages", commaSeparatedList)
.option("-c --collections <collections>", "collections", commaSeparatedList)
.parse(process.argv);

if (program.args.length < 1) {
console.log(
`Usage: localicious validate <localicipe> -r <required languages> -o <optional languages>`
`Usage: localicious validate <localicipe> -r <required languages> -o <optional languages> -c <collections>`
);
process.exit(1);
}
Expand All @@ -24,7 +25,12 @@ const localicipePath = path.resolve(process.cwd(), program.args[0]);

loadYaml(localicipePath)
.flatMap(data => {
return validate(data, program.requiredLanguages, program.optionalLanguages);
return validate(
data,
program.requiredLanguages,
program.optionalLanguages,
program.collections
);
})
.onSuccess(() => {
console.log("✅ Your Localicipe is perfect, keep up the good work!");
Expand Down
10 changes: 2 additions & 8 deletions schemas/schema.json
Expand Up @@ -64,14 +64,8 @@
}
},
"type": "object",
"properties": {
"IOS": {
"$ref": "#/definitions/Node"
},
"ANDROID": {
"$ref": "#/definitions/Node"
},
"SHARED": {
"patternProperties": {
"^.*$": {
"$ref": "#/definitions/Node"
}
},
Expand Down

0 comments on commit cacd8a8

Please sign in to comment.