Giter Club home page Giter Club logo

localicious's Introduction

Localicious

localicious is a toolchain for working with localization files in a platform-agnostic way. With it, you can:

  • 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, iOS or JS from your base localization file

The goals of localicious are:

  • Copywriter-friendliness

    Likewise, it should be easy for copywriters to change and add copy. For each string copywriters should be able to easily get an overview of the translations provided for the various languages.

  • Developer-friendliness

    It should be easy for developers maintaining and developing features to work with the new system. They should be able to trust that the necessary copy will be there for any language. Moreover, the format in which the copy is delivered should be predictable to minimise dependencies between developers and copywriters in a fast-paced environment.

  • Robustness

    One cannot blindly import localization files into the app and expect everything to work. Therefore, localicious enables both validation and conversion. Together, these two operations can support a robust workflow that minimises the potential for mistakes.

Workflow

localicious assumes the following workflow:

  1. You keep all your localizable strings in a YAML file that adheres to the structure defined by localicious.
  2. When committed to a source repository, the YAML file is guaranteed to have passed localicious verification.
  3. You point to the current working version of the YAML file in your iOS or Android project.
  4. Using localicious, you generate the localization files when desired.

Requirements and installation

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 feature and screen. The strings in the Localicipe can be divided into different collections. Multiple collections can be combined when 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:

<COLLECTION NAME>:
  Feature:
    Screen:
      Element:
        COPY:
          en: "Translation for English speakers"
          nl: "Vertaling voor Nederlandstaligen"
        ACCESSIBILITY:
          HINT|LABEL|VALUE:
            en: "Accessibility for English speakers"
            nl: "Toegankelijkheid voor Nederlandstaligen"
      AnotherElement:
        COPY:
          ZERO|ONE|OTHER:
            en: "Plural translation for English speakers"
            nl: "Meervoudige vertaling voor Nederlandstaligen"
        ...
      ...
    ...
  ...
...

Retrieving the Localicipe

If you are working with a team, you probably want to store your Localicipe in a Git repository and manage changes like you handle changes to your source code. Localicious supports that workflow. Simply create a repository that hosts your Localicipe. Then, in the root of the source repository of your Android or iOS project, you add the following LocaliciousConfig.yaml:

source:
  git:
    url: 'https://github.com/localicious/localicious-test.git'
languages:
  - en
  - nl
outputTypes:
  - IOS
collections:
  - IOS
  - 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:

---
# Strings that are used in Android only
ANDROID:
  Checkout:
    OrderOverview:
      Total:
        COPY:
          en: 'Total price: %1{{s}}'  # This placeholder will expand to %1$@ on iOS and %1$s on Android
          nl: 'Totaal: %1{{s}}'
# Strings that are used in iOS only
IOS:
  Settings:
    PushPermissionsRequest:
      Title:
        COPY:
          en: 'Stay up to date'
          nl: 'Blijf op de hoogte'
# Strings that are shared between Android and iOS
SHARED:
  Delivery:
    Widget:
      Title:
        COPY:
          en: "Help"
          nl: "Help"
      SubTitle:
        COPY:
          ZERO:
            en: '%1{{d}} Pending order'
            nl: '%1{{d}} Lopende bestelling'
          ONE:
            en: '%1{{d}} Pending order'
            nl: '%1{{d}} Lopende bestelling'
          OTHER:
            en: '%1{{d}} Pending Orders'
            nl: '%1{{d}} Lopende bestellingen'

By running the following localicious command:

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:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="Checkout.OrderOverview.Total.COPY">Total price: %1$s</string>
  <string name="Delivery.Widget.Title.COPY">Help</string>
  <plurals name="Delivery.Widget.SubTitle.COPY">
    <item quantity="zero">%1$d Pending order</item>
    <item quantity="one">%1$d Pending order</item>
    <item quantity="other">%1$d Pending Orders</item>
  </plurals>
</resources>

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 --outputTypes android --collections ANDROID,SHARED --languages en,nl

By changing the destination output type, like so:

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

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

"Settings.PushPermissionsRequest.Title.COPY" = "Stay up to date";
"Delivery.Widget.Title.COPY" = "Help";
"Delivery.Widget.SubTitle.COPY.ZERO" = "%1$d Pending order";
"Delivery.Widget.SubTitle.COPY.ONE" = "%1$d Pending order";
"Delivery.Widget.SubTitle.COPY.OTHER" = "%1$d Pending Orders";

Validating

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:
  PushPermissionsRequest:
    Subtitle:
      COPY
        en: 'Stay up to date'

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

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:

❌ Your Localicipe contains some issues.

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

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.

localicious's People

Contributors

dependabot[bot] avatar johankool avatar larslockefeer avatar lctwisk avatar leonardowf avatar melisa-dlg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

localicious's Issues

Quotes are not properly escaped on iOS

Steps to reproduce

Given the following Localicipe:

IOS:
  Module:
    Screen:
      Element:
        COPY:
          en: "This is a <a href=\"https://github.com/PicnicSupermarket/localicious\">link</a>"

Observed behaviour

Localicious generates the following Localizable.strings file:

"Module.Screen.Element.COPY" = "This is a <a href="https://github.com/PicnicSupermarket/localicious">link</a>";

This leads to the following error in Xcode: Localizable.strings:1:1: read failed: Couldn't parse property list because the input data was in an invalid format

Expected behaviour

Running plutil leads us to the culprit:

2020-05-28 13:51:12.415 plutil[35933:1269394] CFPropertyListCreateFromXMLData(): Old-style plist parser: missing semicolon in dictionary on line 1. Parsing will be abandoned. Break on _CFPropertyListMissingSemicolon to debug.
Localizable.strings: Unexpected character " at line 1

From this, we can conclude that the following Localizable.strings file is expected by Xcode:

"Module.Screen.Element.COPY" = "This is a <a href=\"https://github.com/PicnicSupermarket/localicious\">link</a>";

iOS code generation can cause runtime exception if number of parameters is not the same as the localization format

Right now, if you by mistake provide one less argument to the Localicious format, the string replacement throws a run time exception.

The problem:

The following example string:
"Screen.Component.Button.ACCESSIBILITY.LABEL" = "%1$@%2$@, hello %3$@.";

when replaced with two arguments result in an EXC_BAD_ACCESS exception

Proposal

Count the number of arguments and placeholders and avoid the exception if they don't match

Duplicate keys in the Localicipe should result in an error

Currently, if you by mistake define duplicate keys in the Localicipe, the previous entries will always be overwritten by the last key in the file.

The problem:
Let's say there's a Localicipe that looks like:

COLLECTION-A:
  Onboarding:
    Login:
      InputField: 
        Placeholder: 
          COPY:
            en: "Username"
      InputField: 
        Placeholder: 
          COPY:
            en: "E-mail"

The key Onboarding.Login.InputField.Placeholder is defined twice, as a result, the second entry will be overwritten by the first one. This behaviour might be confusing, entries that accidentally have duplicated keys will get lost.

Proposal
Improve the validation by throwing a proper error that indicates that duplicated entries are not allowed in the Localicipe.

Incorrect apostrophe encoding on Android

Hello!

We've started to use your toolchain for localization which has been great so far (good work 👍). The only issue we're having is with apostrophes.

Problem
When I use strings that have apostrophes for Android, they are encoded to the XML file using the unicode hex character code. That results in a compile-time error for Android Studio.

To recreate
If I take the example from your blog post (https://blog.picnic.nl/localizing-native-apps-made-easy-with-localicious-5063d02d3511):

ANDROID:
  CounterModule:
    CounterScreen:
      WelcomeLabel:
        COPY:
          en: "We're glad you're here!"

This will turn into the following XML after rendered:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="CounterModule_CounterScreen_WelcomeLabel_COPY">We&amp;apos;re glad you&#x27;re here!</string>
</resources>

That results in the following compile time error for Android Studio:

(108: AAPT: error: unescaped apostrophe in string)

Possible Solutions

  1. Maybe I'm doing something wrong? Is there a proper way to input apostrophes that I've missed?
  2. Surround strings with quotations when exporting them to Android. Change strings_xml_file.hbs to include quotations around value just as you're doing for the string name. And then do one of the following two:
  • Remove any apostrophe conversion logic. The apostrophes will stay as typed into the localicipe. The strings XML output would then be:
    <string name="CounterModule_CounterScreen_WelcomeLabel_COPY">"We're glad you're here!"</string>
  • Change the unicode hex encoding to be HTML encoding instead. &#x27; becomes &apos;. The XML output would be:
    <string name="CounterModule_CounterScreen_WelcomeLabel_COPY">"We&apos;re glad you&apos;re here!"</string>
  1. Add an escape for apostrophes when they are input in the localicpe (similar to how \\n is converted to \n). Then the XML output ends up being:
    <string name="CounterModule_CounterScreen_WelcomeLabel_COPY">We\'re glad you\'re here!</string>

Warn about invalid insertions

This is a {{s}} test fails, because it should have been This is a %1{{s}} test. It would be nice if the validation step checks for this mistake.

Percentage encoding on Android

Hello,

Can percentages be added to the encoding substitution for the Android platform?

Android Studio throws an error for strings that have a percentage in them but aren't format strings. An example:

<string name="over_achiever">"Na het behalen van 100%"</string>

If the % in the above string could become &#37; in the final strings XML file, that should fix the issue. And an exception should be added to not encode the percentage if it is included as part of a format string, as in See all in %1{{s}}

iOS generated code is not public

Steps to reproduce

Given the following Localicipe:

IOS:
  Module:
    Screen:
      Element:
        COPY:
          en: "Hello"

Observed behaviour

Using localicious render example.yaml ./ --languages en --platforms ios
Localicious generates the following Localizable.swift file

...
struct L {
    struct Module {
        struct Screen {
            static let Element = LocaliciousData(
                accessibilityIdentifier: "Module.Screen.Element",
                accessibilityHintKey: nil,
                accessibilityLabelKey: nil,
                accessibilityValueKey: nil,
                translationKey: "Module.Screen.Element.COPY",
                translationArgs: []
            )
        }
    }
}
...

We cannot use the Localizable implementation and the UILabel/UIButton extensions in different targets, because they are not public.

Expected behaviour

Make every definition public

...
public struct L {
    public struct Module {
        public struct Screen {
            public static let Element = LocaliciousData(
                accessibilityIdentifier: "Module.Screen.Element",
                accessibilityHintKey: nil,
                accessibilityLabelKey: nil,
                accessibilityValueKey: nil,
                translationKey: "Module.Screen.Element.COPY",
                translationArgs: []
            )
        }
    }
}
...

Support Moko resources for KMP

I am using this tool in a project where we also use Moko Resources for KMP. The format for android is almost the same but there are some small differences. The main one being that there should be no " around the strings. In my current situation i have written a bash script that will search for all of them and removes them. This isnt a nice solution so i would love to add support for the Moko Resources format directly.

I can create a PR for this but i saw that there is not much activity in this repo anymore. Could anyone let me know wether someone will still take a look at it when i open a pr? Would hate to waste the time of creating this PR and it not being accepted because the repository is dead.

Percentages on iOS

I had one more issue for which I didn't make a PR yet. On iOS the percentage sign has to be encoded as %%. But we don't want to do this for cases where we use it for insertions like %1{{s}}. The regex solution I had kinda works, but does fail when the percentages is at the end of a string, which it unfortunately often is.

So I was for now using a rather hacky approach instead.

const percentageForPlatform = platform => {
  switch (platform) {
    case platformKeywords.ANDROID:
      return "%";  // or "&#37;" ??
    case platformKeywords.IOS:
      return "%%";
  }
};

const substitute = (value, valueSubstitutions, newline, percentage) => {
  // Find all % which are NOT followed with [0...9]{{[s,d]}}
  // and replace with value from percentage
  
  // Official implementation that doesn't work
  // value = value.replace(`/%(\D|$)/gm`, percentage + `$1`)

  // Hacky workaround that works by temporarily replacing % with §
  value = value.split(`%1{{`).join(`§1{{`);
  value = value.split(`%2{{`).join(`§2{{`);
  value = value.split(`%3{{`).join(`§3{{`);
  value = value.split(`%4{{`).join(`§4{{`);
  value = value.split(`%5{{`).join(`§5{{`);
  value = value.split(`%6{{`).join(`§6{{`);
  value = value.split(`%7{{`).join(`§7{{`);
  value = value.split(`%8{{`).join(`§8{{`);
  value = value.split(`%9{{`).join(`§9{{`);
  value = value.split(`%`).join(percentage);
  value = value.split(`§`).join(`%`);

Percentages on iOS in strings with vs. without arguments

Due to the guard on the number of translationArgs, if a string contains a percentage sign, but not any placeholders, the percentage encoding isn't removed because it isn't used as a String format.

So it the yaml contains '42%', it shows up in Localizable.strings as '42%%' and in the UI as '42%%' too, but '%1{{s}} is 42%' comes true correctly.

Removing the guard fixes it.

        let translationArgs = self.translationArgs ?? []
        guard translationArgs.count > 0 else { return value }

        return String(format: value, arguments: translationArgs)

'This command requires one or more output languages.' when --language is set

OS: Ubuntu 23.04

node --version
v16.20.1

yarn --version
1.22.19

localicious --version
1.0.1

When I run
localicious render localizations.yaml ./ -l en,nl -ot ios -c IOS
or
localicious render localizations.yaml ./ --languages en,nl --outputTypes ios --collections IOS
I always get an error stating I did not specify a language:

❌ This command requires one or more output languages.
Usage: localicious render <localicipe> <output_path> -l <languages> -ot <output_types> -c <collections>

Generate additional function without args for quantity strings

Let's take an example of formatting a string for a number of things. The yml could look like this:

ThingCount:
    COPY:
        ZERO:
            en: 'no things'
        ONE:
            en: '%1{{d}} thing'
        OTHER:
            en: '%1{{d}} things

That would generate a function like this:

public static func ThingCount(quantity: Int, args: CVarArg...) -> LocaliciousData {
    ...
}

And you would use it like this:

let numberOfThings: Int = 10
L.Base.ThingCount(quantity: numberOfThings, args: numberOfThings)

My assumption is that the most common use case for strings with number formats would be a single arg so it is a little weird to pass the same arg twice. A nice solution could be to also generate a function that calls the current function with the quantity param as the args like this:

public static func ThingCount(quantity: Int) -> LocaliciousData {
    return ThingCount(quantity: quantity, args: quantity) 
}

Then you get a nice clean looking usage for the simple, single arg case.

let numberOfThings: Int = 10
L.Base.ThingCount(quantity: numberOfThings)

Keys starting with digits do not compile for iOS

This should be caught by validation

For example, this:

SHARED:
    Widget:
      1CoolTitle:
        COPY:
          en: "😎 title"
          nl: "😎 titel"

Renders the following which does not compile:

public struct L {
    public struct Widget {
        public static let 1CoolTitle = LocaliciousData(
            accessibilityIdentifier: "Widget.1CoolTitle",
            accessibilityHintKey: nil,
            accessibilityLabelKey: nil,
            accessibilityValueKey: nil,
            translationKey: "Widget.1CoolTitle.COPY",
            translationArgs: []
        )
    }

Cannot find module 'commander'

Running localicious gives me the following error:

internal/modules/cjs/loader.js:775
    throw err;
    ^

Error: Cannot find module 'commander'
Require stack:
- /Users/lwesterhoff/Developer/forks/localicious/bin/localicious
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:772:15)
    at Function.Module._load (internal/modules/cjs/loader.js:677:27)
    at Module.require (internal/modules/cjs/loader.js:830:19)
    at require (internal/modules/cjs/helpers.js:68:18)
    at Object.<anonymous> (/Users/lwesterhoff/Developer/forks/localicious/bin/localicious:2:17)
    at Module._compile (internal/modules/cjs/loader.js:936:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:947:10)
    at Module.load (internal/modules/cjs/loader.js:790:32)
    at Function.Module._load (internal/modules/cjs/loader.js:703:12)
    at Function.Module.runMain (internal/modules/cjs/loader.js:999:10) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/Users/lwesterhoff/Developer/forks/localicious/bin/localicious' ]
}

Typo on Swift

Hey guys!
After spending some time translating all our copy into a Localicipe file, everything is looking great and the integration was painless! So thank you for that! Except for one little hiccup:

Problem
While installing my config file on my Xcode project, on the generated swift file there is a typo that throws a bunch of errors, nothing big but might be nice to fix it from your side instead of fixing the typo every time we run install 😅

The generated struct looks like this: LocaliciousQuantity(quanitity: quantity)
which of course complains because it should be "quantity": LocaliciousQuantity(quantity: quantity)

Cheers!

iOS code generation always use the main bundle

The current iOS code generation uses an NSLocalizedString to get the translated copy.

An NSLocalizedString always resolves to the main bundle.

The result of invoking localizedStringForKey:value:table: on the main bundle passing nil as the table.

Source: https://developer.apple.com/documentation/foundation/nslocalizedstring

The Problem

I have multiple bundles and I want to be able to choose from which one the string should be loaded.

Proposal

Modify the translation function of the template to accept a bundle parameter, that defaults to the Bundle.main

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.