Typescript Localization and Properties file syntax parser

This is a typescript locale generator written in F# leveraging FParsec.

Localizing an application consists of having language files that will be used to generate strongly typed classes representing a language. The idea is to avoid hardcoding any language text in your actual application. This project wanted to take language properties files that were of the form

# This is an ignored comment, but will be parsed as part of the AST

Property = This is some localized text
OtherProperty = This is localized text with an argument {arg:number}
ThirdProperty = This is more localized text with another argument with no type {arg}	
			  = 
			  = This is a second line		  

Stored in a directory structure like:

locales/
	en-US/
	      global.properties
	      users.properties
	      storeFront.properties
	fr-FR/
	      global.properties
	      users.properties
	      storeFront.properties

The goal is to take each properties file (which I called a "group") and to create strongly typed code for each locale ending up looking like

export class enUStest implements com.devshorts.data.locale.ITest{                         
                                                                                                 
    // localized functions                                                                       
                                                                                                 
    Property():string{                                                                           
        return "This is some localized text";                                                    
    }                                                                                            
                                                                                                 
    OtherProperty(arg:number):string{                                                            
        return "This is localized text with an argument "+arg.toString();                        
    }                                                                                            
                                                                                                 
    ThirdProperty(arg:any):string{                                                              
    	return "This is more localized text with another argument with no type "+arg.toString() 
					+"\r\n"                                                                                 
					+"\r\n"+" This is a second line";                                                                      
	}                                                                                                                                                                                     
                                                                                                 
                                                                                                 
    // dictionary lookup                                                                         
                                                                                                 
    public localeDict:{[id:string] : (...args:any[]) => string;} = {};                           
                                                                                                 
    constructor(){                                                                               
        this.localeDict = {                                                                      
                                                                                                 
			"Property":  $.proxy(this.Property, this),                                                    
			"OtherProperty":  $.proxy(this.OtherProperty, this),                                          
			"ThirdProperty":  $.proxy(this.ThirdProperty, this)                                           
        }                                                                                        
    }                                                                                            
                                                                                                 
}  

With a main language wrapper that looks like. Each group (such as 'users', 'test', or 'global') gets its own class and each language gets an "implementor" class.

module com.devshorts.data.locale {                                                                             
                                                                                                                      
    export class enUS implements com.devshorts.data.locale.ISr {                                               
		     
		test:com.devshorts.data.locale.enUStest = new com.devshorts.data.locale.enUStest();                   
                                                                                                                      
        // locale identifier                                                                                          
                                                                                                                      
        localeType():string{                                                                                          
            return "en-us";                                                                                           
        }                                                                                                             
    }                                                                                                                 
                                                                                                                      
                                                                                                                      
    export class enUStest implements com.devshorts.data.locale.ITest{   
		// ... seen above                                   
	}      
}              

There is also a master interface that you can use to set and reference whatever is the current locale.

/**
* CLASS IS AUTO GENERATED, CHANGES WILL NOT PERSIST
*/

module com.devshorts.data.locale {
    
    export interface ISr{		
		test:ITest;
            
        localeType():string;
    }
  

    export interface ITest{
		Property():string;
		OtherProperty(arg:number):string;
		ThirdProperty(arg:any):string;
        
        localeDict:{[id:string] : (...args:any[]) => string;};
    }
    

}

So the final file system result might look like

locales/
	en-US/
	      global.properties
	      users.properties
	      storeFront.properties
	fr-FR/
	      global.properties
	      users.properties
	      storeFront.properties
com/
	devshorts/
		data/
			locale/
				ISr.ts
				enUSLocale.ts
				frFRLocale.ts

Usage

Usage of the locale generator is: <source locale folders> <generated output folder> <namespace> <master interface name>

So an example might be:

>TypeScriptLocaleWriter.exe locales com/devshorts/data/locale "com.devshorts.data.locale" ISr

In your code, you can maintain a localization singleton via a reference to the interface, such as

private sr:locale.ISr = new locale.enUsLocale();

Or you can do dynamic lookups of localized fields via the dictionary lookup that each group has compiled. This lets you do things like buildling AngularJS localization filters or other dynamic locale lookups.

Language Overrides

You can also configure language overrides that will generate the appropriate typescript locale classes. A language override is where you want to have a superset of a specific language overriding only a certain set of properties.

For example, lets say you have an application geared focused to slightly different markets. 99% of the lexicon is the same, but for a few properties you may want to display something custom. Maybe you are localizing an app that uses the term "cable" but in some market the word "cable" means something different, and you want to just change all the references to "cable" to be "cord". The actual language is the same though, for example en-US. In this case you want to "overload" the property of

cable = cable

To be

cable = cord

The new language name will be en-US-cord. So, your properties may look like:

locale/
	en-us/
		global.properties
		users.properties
	en-us-cord/
		global.properties

Notice you don't need to supply ALL the properties files, and they will NOT be auto generated (like normal languages), since these are only for overriding purposes. The contents of the global.properties in en-us-cord will only have properties that override en-us and no more. What will get generated however, is a typescript class called en-us-cord.ts that implements all of en-us except for what is overloaded by en-us-cord.

You can supply overridable suffixes to the normalizer. If you don't give it any, it will assume you have none.

Implementation

The locale generator is split into two sections. The first is a locale parser, which creates an AST using FParsec combinators. The first part will also normalize all locales. For example, if you add a property to the master language (right now its "en-US" but it is configurable), you want that property to be added to the .properties. files of all the other languages. Same if you remove a property, you want that property to be removed.

Using the first section you can get direct access to the properties AST, so you can implement other writers or do other syntax manipulation. Comment information is also stored in the AST so you can leverage that information as well if you need to.

The syntax tree that you will get back will be a LocaleElement list where a LocaleElement is a discriminated union that looks like

type Name = string

type Type = string

type PropertyName = string

type Arg = 
    | WithType of Name * Type
    | NoType of Name

type LocaleContents = 
    | Argument of Arg
    | Text of string    
    | Comment of string
    | NewLine

type LocaleProperty = (PropertyName * LocaleContents list)

type LocaleElement =
    | Entry of LocaleProperty
    | IgnoreEntry of LocaleContents

The second part is the typescript writer, which takes the normalized locales and their groups, generates class and interface implementations, and merges all the files into the appropriate final form.