diff --git a/.gitignore b/.gitignore index fd7400f..40c9404 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ elm-stuff node_modules out -example/src/Translation linter-logs build diff --git a/Makefile b/Makefile index ca8b70d..a52a9ba 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,12 @@ dist: $(ELM_FILES) elm-make src/Main.elm --output dist/elm.js +gentest: dist + cd example && node ../extractor.js -l De,En -s --root src + +imptest: dist + cd example && node ../extractor.js --format CSV --language En --importOutput src/ --import languages/en.csv + test: ## Run tests ./node_modules/elm-test/bin/elm-test diff --git a/README.md b/README.md index a400956..2c4a390 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,75 @@ elm-i18n-generator --format PO --language De --import export.po Results in the same `import/De/Translation/Main.elm` as in the [CSV example](#import-generate-elm-source-code-from-csv). + +#### Generate Elm source for switching Language during run-time + +Pass all the languages you want to switch between as a comma-separated list. + +```bash +elm-i18n-generator --language De,Pl,En,Fr --genswitch +``` + +#### Migrate legacy symlinked translations to switchables + +Remove the legacy symlink +``` +rm src/Translation +``` + +Export each of your languages to CSV temporarily +``` +mkdir languages +elm-i18n.generator --format CSV --language En --root Translation/ --export --exportOutput languages/en.csv +``` + +Then, import them again. +``` +elm-i18n-generator --format CSV --language En --importOutput src --import languages/en.csv +``` + + +Finally, generate the switch +``` +elm-i18n-generator --language De,En --genswitch --importOutput . --root src +``` + +After this your Project should look like this: + +``` +project +└── src/ + ├── Main.elm (e.g. imports Translation.Main) + ├── View.elm (e.g. imports Translation.View) + └── Translation + ├── Main.elm (module Translation.Main) + ├── View.elm (module Translation.View) + ├── Main/ + │ ├── De.elm (module Translation.Main.De) + │ └── En.elm (module Translation.View.En) + └── View/ + ├── De.elm (module Translation.Main.De) + └── En.elm (module Translation.View.En) +``` + +Use the `Language` type to dynamically translate your websites: + + +``` +import Translation exposing (Language(..)) + +state = + { language = Language + } + +render state = + div [] + [ text (Translation.Main.greetingWithName state.language "Leonard") + , text "and in German:" + , text (Translation.Main.greetingWithName De "Leonard") + ] +``` + ## Advantages + Each build of your app only contains one language. diff --git a/dist/elm.js b/dist/elm.js index 330a6e3..227101b 100644 --- a/dist/elm.js +++ b/dist/elm.js @@ -9190,12 +9190,35 @@ var _iosphere$elm_i18n$CSV_Template$placeholder = function (placeholder) { }; var _iosphere$elm_i18n$CSV_Template$headers = 'Module,Key,Comment,Supported Placeholders,Translation'; +var _iosphere$elm_i18n$Localized$namedModule = function (name) { + return { + ctor: '_Tuple2', + _0: name, + _1: {ctor: '[]'} + }; +}; +var _iosphere$elm_i18n$Localized$languageModuleName = F2( + function (name, lang) { + return A2( + _elm_lang$core$Basics_ops['++'], + name, + A2(_elm_lang$core$Basics_ops['++'], '.', lang)); + }); +var _iosphere$elm_i18n$Localized$elementMeta = F2( + function (accessor, element) { + var _p0 = element; + if (_p0.ctor === 'ElementStatic') { + return accessor(_p0._0.meta); + } else { + return accessor(_p0._0.meta); + } + }); var _iosphere$elm_i18n$Localized$isEmptyFormatComponent = function (comp) { - var _p0 = comp; - if (_p0.ctor === 'FormatComponentStatic') { - return _elm_lang$core$String$isEmpty(_p0._0); + var _p1 = comp; + if (_p1.ctor === 'FormatComponentStatic') { + return _elm_lang$core$String$isEmpty(_p1._0); } else { - return _elm_lang$core$String$isEmpty(_p0._0); + return _elm_lang$core$String$isEmpty(_p1._0); } }; var _iosphere$elm_i18n$Localized$Meta = F3( @@ -9216,6 +9239,48 @@ var _iosphere$elm_i18n$Localized$ElementFormat = function (a) { var _iosphere$elm_i18n$Localized$ElementStatic = function (a) { return {ctor: 'ElementStatic', _0: a}; }; +var _iosphere$elm_i18n$Localized$elementRemoveLang = F2( + function (lang, element) { + var changeName = F2( + function (meta, name) { + return _elm_lang$core$Native_Utils.update( + meta, + {moduleName: name}); + }); + var moduleName = A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.moduleName; + }, + element); + var cleanedName = A2( + _elm_lang$core$String$join, + '.', + A2( + _elm_lang$core$List$filter, + function (p) { + return !_elm_lang$core$Native_Utils.eq(p, lang); + }, + A2(_elm_lang$core$String$split, '.', moduleName))); + var _p2 = element; + if (_p2.ctor === 'ElementStatic') { + var _p3 = _p2._0; + return _iosphere$elm_i18n$Localized$ElementStatic( + _elm_lang$core$Native_Utils.update( + _p3, + { + meta: A2(changeName, _p3.meta, cleanedName) + })); + } else { + var _p4 = _p2._0; + return _iosphere$elm_i18n$Localized$ElementFormat( + _elm_lang$core$Native_Utils.update( + _p4, + { + meta: A2(changeName, _p4.meta, cleanedName) + })); + } + }); var _iosphere$elm_i18n$Localized$FormatComponentPlaceholder = function (a) { return {ctor: 'FormatComponentPlaceholder', _0: a}; }; @@ -9789,62 +9854,58 @@ var _iosphere$elm_i18n$Localized_Parser$parse = function (source) { stringKeysAndParameters); }; -var _iosphere$elm_i18n$Localized_Writer$comment = function (string) { - return _elm_lang$core$String$isEmpty(string) ? '' : A2( +var _iosphere$elm_i18n$Localized_Writer_Module$importModuleExposingAll = function (_p0) { + var _p1 = _p0; + return A2( _elm_lang$core$Basics_ops['++'], - '{-| ', - A2(_elm_lang$core$Basics_ops['++'], string, '\n-}\n')); + 'import ', + A2(_elm_lang$core$Basics_ops['++'], _p1._0, ' exposing (..)\n')); }; -var _iosphere$elm_i18n$Localized_Writer$signature = F2( - function (key, placeholders) { - var num = _elm_lang$core$List$length(placeholders); - var types = _elm_lang$core$Native_Utils.eq(num, 0) ? 'String' : A2( - _elm_lang$core$String$join, - ' -> ', - A2(_elm_lang$core$List$repeat, num + 1, 'String')); - var parameters = _elm_lang$core$Native_Utils.eq(num, 0) ? '' : A2( - _elm_lang$core$Basics_ops['++'], - ' ', - A2(_elm_lang$core$String$join, ' ', placeholders)); - return A2( - _elm_lang$core$Basics_ops['++'], - A2( - _elm_lang$core$Basics_ops['++'], - key, - A2( - _elm_lang$core$Basics_ops['++'], - ' : ', - A2(_elm_lang$core$Basics_ops['++'], types, '\n'))), - A2( - _elm_lang$core$Basics_ops['++'], - key, - A2(_elm_lang$core$Basics_ops['++'], parameters, ' ='))); - }); -var _iosphere$elm_i18n$Localized_Writer$tab = ' '; -var _iosphere$elm_i18n$Localized_Writer$functionStatic = function (staticLocalized) { +var _iosphere$elm_i18n$Localized_Writer_Module$importModule = function (_p2) { + var _p3 = _p2; return A2( _elm_lang$core$Basics_ops['++'], - _iosphere$elm_i18n$Localized_Writer$comment(staticLocalized.meta.comment), - A2( + 'import ', + A2(_elm_lang$core$Basics_ops['++'], _p3._0, '\n')); +}; +var _iosphere$elm_i18n$Localized_Writer_Module$head = function (_p4) { + var _p5 = _p4; + return A2( + _elm_lang$core$Basics_ops['++'], + 'module ', + A2(_elm_lang$core$Basics_ops['++'], _p5._0, ' exposing (..)\n\n{-| -}\n\n')); +}; +var _iosphere$elm_i18n$Localized_Writer_Module$elements = F2( + function (functionImplementation, _p6) { + var _p7 = _p6; + return A3( + _elm_lang$core$Basics$flip, + _elm_lang$core$String$append, + '\n', + _elm_lang$core$String$trim( + A2( + _elm_lang$core$String$join, + '\n\n\n', + A2(_elm_lang$core$List$map, functionImplementation, _p7._1)))); + }); +var _iosphere$elm_i18n$Localized_Writer_Module$implementation = F2( + function (functionImplementation, mod) { + return A2( _elm_lang$core$Basics_ops['++'], - A2( - _iosphere$elm_i18n$Localized_Writer$signature, - staticLocalized.meta.key, - {ctor: '[]'}), + _iosphere$elm_i18n$Localized_Writer_Module$head(mod), A2( _elm_lang$core$Basics_ops['++'], '\n', - A2( - _elm_lang$core$Basics_ops['++'], - _iosphere$elm_i18n$Localized_Writer$tab, - _elm_lang$core$Basics$toString(staticLocalized.value))))); -}; -var _iosphere$elm_i18n$Localized_Writer$formatComponentsImplementation = F2( + A2(_iosphere$elm_i18n$Localized_Writer_Module$elements, functionImplementation, mod))); + }); + +var _iosphere$elm_i18n$Localized_Writer_Element$tab = ' '; +var _iosphere$elm_i18n$Localized_Writer_Element$formatComponentsImplementation = F2( function (index, component) { - var prefix = _elm_lang$core$Native_Utils.eq(index, 0) ? _iosphere$elm_i18n$Localized_Writer$tab : A2( + var prefix = _elm_lang$core$Native_Utils.eq(index, 0) ? _iosphere$elm_i18n$Localized_Writer_Element$tab : A2( _elm_lang$core$Basics_ops['++'], - _iosphere$elm_i18n$Localized_Writer$tab, - A2(_elm_lang$core$Basics_ops['++'], _iosphere$elm_i18n$Localized_Writer$tab, '++ ')); + _iosphere$elm_i18n$Localized_Writer_Element$tab, + A2(_elm_lang$core$Basics_ops['++'], _iosphere$elm_i18n$Localized_Writer_Element$tab, '++ ')); var _p0 = component; if (_p0.ctor === 'FormatComponentStatic') { return A2( @@ -9858,61 +9919,364 @@ var _iosphere$elm_i18n$Localized_Writer$formatComponentsImplementation = F2( _elm_lang$core$String$trim(_p0._0)); } }); -var _iosphere$elm_i18n$Localized_Writer$functionFormat = function (format) { +var _iosphere$elm_i18n$Localized_Writer_Element$head = function (element) { return A2( _elm_lang$core$Basics_ops['++'], - _iosphere$elm_i18n$Localized_Writer$comment(format.meta.comment), + A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.key; + }, + element), A2( _elm_lang$core$Basics_ops['++'], - A2(_iosphere$elm_i18n$Localized_Writer$signature, format.meta.key, format.placeholders), - A2( - _elm_lang$core$Basics_ops['++'], - '\n', - A2( - _elm_lang$core$String$join, - '\n', - A2(_elm_lang$core$List$indexedMap, _iosphere$elm_i18n$Localized_Writer$formatComponentsImplementation, format.components))))); + function () { + var _p1 = element; + if (_p1.ctor === 'ElementStatic') { + return ''; + } else { + return A2( + _elm_lang$core$Basics_ops['++'], + ' ', + A2(_elm_lang$core$String$join, '', _p1._0.placeholders)); + } + }(), + ' =')); }; -var _iosphere$elm_i18n$Localized_Writer$functionFromElement = function (element) { - var _p1 = element; - if (_p1.ctor === 'ElementStatic') { - return _iosphere$elm_i18n$Localized_Writer$functionStatic(_p1._0); +var _iosphere$elm_i18n$Localized_Writer_Element$body = function (element) { + var _p2 = element; + if (_p2.ctor === 'ElementStatic') { + return A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer_Element$tab, + _elm_lang$core$Basics$toString(_p2._0.value)); } else { - return _iosphere$elm_i18n$Localized_Writer$functionFormat(_p1._0); + return A2( + _elm_lang$core$String$join, + '\n', + A2(_elm_lang$core$List$indexedMap, _iosphere$elm_i18n$Localized_Writer_Element$formatComponentsImplementation, _p2._0.components)); } }; -var _iosphere$elm_i18n$Localized_Writer$moduleImplementation = F2( - function (name, elements) { +var _iosphere$elm_i18n$Localized_Writer_Element$placeholders = function (element) { + var _p3 = element; + if (_p3.ctor === 'ElementStatic') { + return 'String'; + } else { + var num = _elm_lang$core$List$length(_p3._0.placeholders); return A2( + _elm_lang$core$String$join, + ' -> ', + A2(_elm_lang$core$List$repeat, num + 1, 'String')); + } +}; +var _iosphere$elm_i18n$Localized_Writer_Element$typeDeclaration = function (element) { + return A2( + _elm_lang$core$Basics_ops['++'], + A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.key; + }, + element), + A2( + _elm_lang$core$Basics_ops['++'], + ' : ', + _iosphere$elm_i18n$Localized_Writer_Element$placeholders(element))); +}; + +var _iosphere$elm_i18n$Localized_Switch$removeLocale = F2( + function (langs, element) { + return A3(_elm_lang$core$List$foldr, _iosphere$elm_i18n$Localized$elementRemoveLang, element, langs); + }); +var _iosphere$elm_i18n$Localized_Switch$mainModule = function (languages) { + var name = 'Translation'; + var mod = { + ctor: '_Tuple2', + _0: name, + _1: {ctor: '[]'} + }; + return { + ctor: '_Tuple2', + _0: name, + _1: A2( _elm_lang$core$Basics_ops['++'], - 'module ', + _iosphere$elm_i18n$Localized_Writer_Module$head(mod), A2( _elm_lang$core$Basics_ops['++'], - name, + 'type Language = ', A2( _elm_lang$core$Basics_ops['++'], - ' exposing (..)\n\n{-| -}\n\n\n', - A3( - _elm_lang$core$Basics$flip, - _elm_lang$core$String$append, + A2(_elm_lang$core$String$join, ' | ', languages), + '\n'))) + }; +}; +var _iosphere$elm_i18n$Localized_Switch$elementSource = F2( + function (languages, element) { + var placeholders = _iosphere$elm_i18n$Localized_Writer_Element$placeholders(element); + var moduleName = A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.moduleName; + }, + element); + var name = A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.key; + }, + element); + return A2( + _elm_lang$core$Basics_ops['++'], + name, + A2( + _elm_lang$core$Basics_ops['++'], + ' : Language -> ', + A2( + _elm_lang$core$Basics_ops['++'], + placeholders, + A2( + _elm_lang$core$Basics_ops['++'], '\n', - _elm_lang$core$String$trim( + A2( + _elm_lang$core$Basics_ops['++'], + name, A2( - _elm_lang$core$String$join, - '\n\n\n', - A2(_elm_lang$core$List$map, _iosphere$elm_i18n$Localized_Writer$functionFromElement, elements))))))); - }); -var _iosphere$elm_i18n$Localized_Writer$generate = _elm_lang$core$List$map( - function (_p2) { - var _p3 = _p2; - var _p4 = _p3._0; + _elm_lang$core$Basics_ops['++'], + ' language =\n', + A2( + _elm_lang$core$Basics_ops['++'], + A2(_elm_lang$core$Basics_ops['++'], _iosphere$elm_i18n$Localized_Writer_Element$tab, 'case language of\n'), + A2( + _elm_lang$core$String$join, + '\n', + A2( + _elm_lang$core$List$map, + function (l) { + return A2( + _elm_lang$core$Basics_ops['++'], + A2(_elm_lang$core$Basics_ops['++'], _iosphere$elm_i18n$Localized_Writer_Element$tab, _iosphere$elm_i18n$Localized_Writer_Element$tab), + A2( + _elm_lang$core$Basics_ops['++'], + l, + A2( + _elm_lang$core$Basics_ops['++'], + ' -> ', + A2( + _elm_lang$core$Basics_ops['++'], + moduleName, + A2( + _elm_lang$core$Basics_ops['++'], + '.', + A2( + _elm_lang$core$Basics_ops['++'], + l, + A2(_elm_lang$core$Basics_ops['++'], '.', name))))))); + }, + languages))))))))); + }); +var _iosphere$elm_i18n$Localized_Switch$switchSource = F2( + function (languages, mod) { + var _p0 = mod; + var moduleName = _p0._0; return { ctor: '_Tuple2', - _0: _p4, - _1: A2(_iosphere$elm_i18n$Localized_Writer$moduleImplementation, _p4, _p3._1) + _0: moduleName, + _1: A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer_Module$head(mod), + A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer_Module$importModuleExposingAll( + { + ctor: '_Tuple2', + _0: 'Translation', + _1: {ctor: '[]'} + }), + A2( + _elm_lang$core$Basics_ops['++'], + A2( + _elm_lang$core$String$join, + '', + A2( + _elm_lang$core$List$map, + function (_p1) { + return _iosphere$elm_i18n$Localized_Writer_Module$importModule( + _iosphere$elm_i18n$Localized$namedModule( + A2(_iosphere$elm_i18n$Localized$languageModuleName, moduleName, _p1))); + }, + languages)), + A2( + _elm_lang$core$Basics_ops['++'], + '\n\n', + A2( + _iosphere$elm_i18n$Localized_Writer_Module$elements, + _iosphere$elm_i18n$Localized_Switch$elementSource(languages), + mod))))) + }; + }); +var _iosphere$elm_i18n$Localized_Switch$indexBy = F2( + function (keymaker, elements) { + return A3( + _elm_lang$core$List$foldr, + F2( + function (e, d) { + return A3( + _elm_lang$core$Dict$update, + keymaker(e), + function (v) { + var _p2 = v; + if (_p2.ctor === 'Nothing') { + return _elm_lang$core$Maybe$Just( + { + ctor: '::', + _0: e, + _1: {ctor: '[]'} + }); + } else { + return _elm_lang$core$Maybe$Just( + {ctor: '::', _0: e, _1: _p2._0}); + } + }, + d); + }), + _elm_lang$core$Dict$empty, + elements); + }); +var _iosphere$elm_i18n$Localized_Switch$member = F2( + function (e, list) { + var sameElement = F2( + function (e1, e2) { + var _p3 = {ctor: '_Tuple2', _0: e1, _1: e2}; + if (_p3._0.ctor === 'ElementStatic') { + if (_p3._1.ctor === 'ElementFormat') { + return false; + } else { + var _p5 = _p3._1._0; + var _p4 = _p3._0._0; + return _elm_lang$core$Native_Utils.eq(_p4.meta.moduleName, _p5.meta.moduleName) && _elm_lang$core$Native_Utils.eq(_p4.meta.key, _p5.meta.key); + } + } else { + if (_p3._1.ctor === 'ElementStatic') { + return false; + } else { + var _p7 = _p3._1._0; + var _p6 = _p3._0._0; + return _elm_lang$core$Native_Utils.eq(_p6.meta.moduleName, _p7.meta.moduleName) && _elm_lang$core$Native_Utils.eq(_p6.meta.key, _p7.meta.key); + } + } + }); + return A2( + _elm_lang$core$List$any, + sameElement(e), + list); + }); +var _iosphere$elm_i18n$Localized_Switch$flatten2D = function (list) { + return A3( + _elm_lang$core$List$foldr, + F2( + function (x, y) { + return A2(_elm_lang$core$Basics_ops['++'], x, y); + }), + {ctor: '[]'}, + list); +}; +var _iosphere$elm_i18n$Localized_Switch$u = F2( + function (list, have) { + u: + while (true) { + var _p8 = list; + if (_p8.ctor === '::') { + var _p10 = _p8._1; + var _p9 = _p8._0; + if (A2(_iosphere$elm_i18n$Localized_Switch$member, _p9, have)) { + var _v3 = _p10, + _v4 = have; + list = _v3; + have = _v4; + continue u; + } else { + var _v5 = _p10, + _v6 = {ctor: '::', _0: _p9, _1: have}; + list = _v5; + have = _v6; + continue u; + } + } else { + return have; + } + } + }); +var _iosphere$elm_i18n$Localized_Switch$unique = function (elements) { + return A2( + _iosphere$elm_i18n$Localized_Switch$u, + elements, + {ctor: '[]'}); +}; +var _iosphere$elm_i18n$Localized_Switch$generate = F2( + function (languages, sources) { + return { + ctor: '::', + _0: _iosphere$elm_i18n$Localized_Switch$mainModule(languages), + _1: A2( + _elm_lang$core$List$map, + _iosphere$elm_i18n$Localized_Switch$switchSource(languages), + _elm_lang$core$Dict$toList( + A2( + _iosphere$elm_i18n$Localized_Switch$indexBy, + _iosphere$elm_i18n$Localized$elementMeta( + function (_) { + return _.moduleName; + }), + _iosphere$elm_i18n$Localized_Switch$unique( + A2( + _elm_lang$core$List$map, + _iosphere$elm_i18n$Localized_Switch$removeLocale(languages), + _iosphere$elm_i18n$Localized_Switch$flatten2D( + A2(_elm_lang$core$List$map, _iosphere$elm_i18n$Localized_Parser$parse, sources))))))) }; }); +var _iosphere$elm_i18n$Localized_Writer$comment = function (string) { + return _elm_lang$core$String$isEmpty(string) ? '' : A2( + _elm_lang$core$Basics_ops['++'], + '{-| ', + A2(_elm_lang$core$Basics_ops['++'], string, '\n-}\n')); +}; +var _iosphere$elm_i18n$Localized_Writer$element = function (element) { + var c = A2( + _iosphere$elm_i18n$Localized$elementMeta, + function (_) { + return _.comment; + }, + element); + return A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer$comment(c), + A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer_Element$typeDeclaration(element), + A2( + _elm_lang$core$Basics_ops['++'], + '\n', + A2( + _elm_lang$core$Basics_ops['++'], + _iosphere$elm_i18n$Localized_Writer_Element$head(element), + A2( + _elm_lang$core$Basics_ops['++'], + '\n', + _iosphere$elm_i18n$Localized_Writer_Element$body(element)))))); +}; +var _iosphere$elm_i18n$Localized_Writer$moduleImplementation = function (mod) { + var _p0 = mod; + var moduleName = _p0._0; + return { + ctor: '_Tuple2', + _0: moduleName, + _1: A2(_iosphere$elm_i18n$Localized_Writer_Module$implementation, _iosphere$elm_i18n$Localized_Writer$element, mod) + }; +}; +var _iosphere$elm_i18n$Localized_Writer$generate = _elm_lang$core$List$map(_iosphere$elm_i18n$Localized_Writer$moduleImplementation); + var _iosphere$elm_i18n$PO_Template$placeholderCommentPrefix = 'i18n: placeholders: '; var _iosphere$elm_i18n$PO_Template$placeholder = function (placeholder) { return A2( @@ -10336,8 +10700,19 @@ var _iosphere$elm_i18n$PO_Import$generate = function (poString) { keysInModules); }; +var _iosphere$elm_i18n$Main$addLanguageToModuleName = function (lang) { + return _elm_lang$core$Tuple$mapFirst( + A2(_elm_lang$core$Basics$flip, _iosphere$elm_i18n$Localized$languageModuleName, lang)); +}; +var _iosphere$elm_i18n$Main$slashifyModuleName = _elm_lang$core$Tuple$mapFirst( + function (_p0) { + return A2( + _elm_lang$core$String$join, + '/', + A2(_elm_lang$core$String$split, '.', _p0)); + }); var _iosphere$elm_i18n$Main$update = F2( - function (_p0, model) { + function (_p1, model) { return {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; }); var _iosphere$elm_i18n$Main$exportResult = _elm_lang$core$Native_Platform.outgoingPort( @@ -10348,8 +10723,8 @@ var _iosphere$elm_i18n$Main$exportResult = _elm_lang$core$Native_Platform.outgoi var _iosphere$elm_i18n$Main$operationExport = F2( function (source, format) { var exportFunction = function () { - var _p1 = format; - if (_p1.ctor === 'CSV') { + var _p2 = format; + if (_p2.ctor === 'CSV') { return _iosphere$elm_i18n$CSV_Export$generate; } else { return _iosphere$elm_i18n$PO_Export$generate; @@ -10368,37 +10743,54 @@ var _iosphere$elm_i18n$Main$importResult = _elm_lang$core$Native_Platform.outgoi return [v._0, v._1]; }); }); -var _iosphere$elm_i18n$Main$operationImport = F2( - function (csv, format) { +var _iosphere$elm_i18n$Main$operationImport = F3( + function (csv, mlangs, format) { var importFunction = function () { - var _p2 = format; - if (_p2.ctor === 'CSV') { + var _p3 = format; + if (_p3.ctor === 'CSV') { return _iosphere$elm_i18n$CSV_Import$generate; } else { return _iosphere$elm_i18n$PO_Import$generate; } }(); + var lang = A2( + _elm_lang$core$Maybe$withDefault, + 'Klingon', + _elm_lang$core$List$head( + A2( + _elm_lang$core$Maybe$withDefault, + {ctor: '[]'}, + mlangs))); return _iosphere$elm_i18n$Main$importResult( A2( _elm_lang$core$List$map, - _elm_lang$core$Tuple$mapFirst( - function (_p3) { - return A2( - _elm_lang$core$String$join, - '/', - A2(_elm_lang$core$String$split, '.', _p3)); - }), + _iosphere$elm_i18n$Main$slashifyModuleName, _iosphere$elm_i18n$Localized_Writer$generate( - importFunction( - A2( - _elm_lang$core$Maybe$withDefault, - '', - _elm_lang$core$List$head(csv)))))); + A2( + _elm_lang$core$List$map, + _iosphere$elm_i18n$Main$addLanguageToModuleName(lang), + importFunction( + A2( + _elm_lang$core$Maybe$withDefault, + '', + _elm_lang$core$List$head(csv))))))); + }); +var _iosphere$elm_i18n$Main$operationGenerateSwitch = F2( + function (sources, mlangs) { + var locales = A2( + _elm_lang$core$Maybe$withDefault, + {ctor: '[]'}, + mlangs); + return _iosphere$elm_i18n$Main$importResult( + A2( + _elm_lang$core$List$map, + _iosphere$elm_i18n$Main$slashifyModuleName, + A2(_iosphere$elm_i18n$Localized_Switch$generate, locales, sources))); }); var _iosphere$elm_i18n$Main$Model = {}; -var _iosphere$elm_i18n$Main$Flags = F3( - function (a, b, c) { - return {sources: a, operation: b, format: c}; +var _iosphere$elm_i18n$Main$Flags = F4( + function (a, b, c, d) { + return {sources: a, operation: b, format: c, languages: d}; }); var _iosphere$elm_i18n$Main$PO = {ctor: 'PO'}; var _iosphere$elm_i18n$Main$CSV = {ctor: 'CSV'}; @@ -10408,6 +10800,9 @@ var _iosphere$elm_i18n$Main$formatFromString = function (maybeFormat) { formatString, _elm_lang$core$Maybe$Just('PO')) ? _iosphere$elm_i18n$Main$PO : _iosphere$elm_i18n$Main$CSV; }; +var _iosphere$elm_i18n$Main$GenSwitch = function (a) { + return {ctor: 'GenSwitch', _0: a}; +}; var _iosphere$elm_i18n$Main$Import = function (a) { return {ctor: 'Import', _0: a}; }; @@ -10416,23 +10811,40 @@ var _iosphere$elm_i18n$Main$Export = function (a) { }; var _iosphere$elm_i18n$Main$operationFromString = F2( function (operation, formatString) { - return (_elm_lang$core$Native_Utils.eq(operation, 'import') ? _iosphere$elm_i18n$Main$Import : _iosphere$elm_i18n$Main$Export)( + return function () { + var _p4 = operation; + switch (_p4) { + case 'import': + return _iosphere$elm_i18n$Main$Import; + case 'export': + return _iosphere$elm_i18n$Main$Export; + default: + return _iosphere$elm_i18n$Main$GenSwitch; + } + }()( _iosphere$elm_i18n$Main$formatFromString(formatString)); }); var _iosphere$elm_i18n$Main$init = function (flags) { - var _p4 = A2(_iosphere$elm_i18n$Main$operationFromString, flags.operation, flags.format); - if (_p4.ctor === 'Export') { - return { - ctor: '_Tuple2', - _0: {}, - _1: A2(_iosphere$elm_i18n$Main$operationExport, flags.sources, _p4._0) - }; - } else { - return { - ctor: '_Tuple2', - _0: {}, - _1: A2(_iosphere$elm_i18n$Main$operationImport, flags.sources, _p4._0) - }; + var _p5 = A2(_iosphere$elm_i18n$Main$operationFromString, flags.operation, flags.format); + switch (_p5.ctor) { + case 'Export': + return { + ctor: '_Tuple2', + _0: {}, + _1: A2(_iosphere$elm_i18n$Main$operationExport, flags.sources, _p5._0) + }; + case 'Import': + return { + ctor: '_Tuple2', + _0: {}, + _1: A3(_iosphere$elm_i18n$Main$operationImport, flags.sources, flags.languages, _p5._0) + }; + default: + return { + ctor: '_Tuple2', + _0: {}, + _1: A2(_iosphere$elm_i18n$Main$operationGenerateSwitch, flags.sources, flags.languages) + }; } }; var _iosphere$elm_i18n$Main$main = _elm_lang$core$Platform$programWithFlags( @@ -10446,19 +10858,39 @@ var _iosphere$elm_i18n$Main$main = _elm_lang$core$Platform$programWithFlags( function (format) { return A2( _elm_lang$core$Json_Decode$andThen, - function (operation) { + function (languages) { return A2( _elm_lang$core$Json_Decode$andThen, - function (sources) { - return _elm_lang$core$Json_Decode$succeed( - {format: format, operation: operation, sources: sources}); + function (operation) { + return A2( + _elm_lang$core$Json_Decode$andThen, + function (sources) { + return _elm_lang$core$Json_Decode$succeed( + {format: format, languages: languages, operation: operation, sources: sources}); + }, + A2( + _elm_lang$core$Json_Decode$field, + 'sources', + _elm_lang$core$Json_Decode$list(_elm_lang$core$Json_Decode$string))); }, - A2( - _elm_lang$core$Json_Decode$field, - 'sources', - _elm_lang$core$Json_Decode$list(_elm_lang$core$Json_Decode$string))); + A2(_elm_lang$core$Json_Decode$field, 'operation', _elm_lang$core$Json_Decode$string)); }, - A2(_elm_lang$core$Json_Decode$field, 'operation', _elm_lang$core$Json_Decode$string)); + A2( + _elm_lang$core$Json_Decode$field, + 'languages', + _elm_lang$core$Json_Decode$oneOf( + { + ctor: '::', + _0: _elm_lang$core$Json_Decode$null(_elm_lang$core$Maybe$Nothing), + _1: { + ctor: '::', + _0: A2( + _elm_lang$core$Json_Decode$map, + _elm_lang$core$Maybe$Just, + _elm_lang$core$Json_Decode$list(_elm_lang$core$Json_Decode$string)), + _1: {ctor: '[]'} + } + }))); }, A2( _elm_lang$core$Json_Decode$field, diff --git a/example/Translation/De/Main.elm b/example/src/Translation/Main/De.elm similarity index 85% rename from example/Translation/De/Main.elm rename to example/src/Translation/Main/De.elm index e923f7f..f230033 100644 --- a/example/Translation/De/Main.elm +++ b/example/src/Translation/Main/De.elm @@ -1,4 +1,4 @@ -module Translation.Main exposing (..) +module Translation.Main.De exposing (..) {-| -} diff --git a/example/Translation/En/Main.elm b/example/src/Translation/Main/En.elm similarity index 54% rename from example/Translation/En/Main.elm rename to example/src/Translation/Main/En.elm index 7962c4e..d772398 100644 --- a/example/Translation/En/Main.elm +++ b/example/src/Translation/Main/En.elm @@ -1,4 +1,4 @@ -module Translation.Main exposing (..) +module Translation.Main.En exposing (..) {-| -} @@ -10,9 +10,6 @@ greeting = "Hello" -{-| A personalized greeting. Note to transaltor: Use {{name}} as a placeholder -for the user's name. --} greetingWithName : String -> String greetingWithName name = "Hello, " diff --git a/extractor.js b/extractor.js index bcf3b33..a42c2c3 100755 --- a/extractor.js +++ b/extractor.js @@ -8,6 +8,7 @@ const argv = require("yargs") .option("format", {default: "csv", describe: "The format of the import/export operation. Supported formats are CSV and PO (case-insensitive)."}) .option("import", {alias: "i", describe: "A CSV file to be imported and to generate code from. Generate elm files will be placed in ."}) .option("importOutput", {default: "import", describe: "The base directory to which the generated code should be written. Subdirectories will be created per language and submodule."}) + .option("genswitch", {alias: "s", describe: "Generates Elm modules containing switches for all given languages."}) .option("language", {alias: "l", describe: "The language code of the current operation. This should match the subdirectory of the language in root."}) .option("root", {default: "Translation", describe: "The root to the translation modules. This script expects this directory to contain a subdirectory for each language."}) .demand("language") @@ -19,8 +20,8 @@ const fs = require("fs-extra"); const path = require("path"); const glob = require("glob"); -if (!argv.export && !argv.import) { - console.error("Please provide import or export option"); +if (!argv.export && !argv.import && !argv.genswitch) { + console.error("Please provide import, export or genswitch option"); process.exit(403); } @@ -46,6 +47,7 @@ if (argv.export) { // pass the array of file contents to our elm worker let worker = Elm.Main.worker({ + "languages": [], "sources": fileContents, "operation": "export", "format": argv.format, @@ -55,7 +57,7 @@ if (argv.export) { worker.ports.exportResult.subscribe(function(resultString) { handleExport(resultString); }); -} else { +} else if (argv.import) { // ensure that import is a valid file path if (!argv.import || argv.import == "" || argv.import == true) { console.error("Please provide an import path"); @@ -72,13 +74,40 @@ if (argv.export) { let csvContent = data.toString(); let worker = Elm.Main.worker({ + "languages": argv.language.split(","), "sources": [csvContent], "operation": "import", "format": argv.format, }); + let importDir = path.join(currentDir, argv.importOutput, argv.root); + worker.ports.importResult.subscribe(function(resultString) { + handleImport(resultString, importDir); + }); +} else { + let fullPath = path.join(currentDir, argv.root); + console.log("Parsing from", fullPath); + let fileNames = glob.sync(fullPath + "/Translation/**/{"+argv.language+"}.elm"); + console.log("└── Found elm module files for export:", fileNames); + + // read all files and store their content in an array + let fileContents = []; + fileNames.forEach(function(file) { + let data = fs.readFileSync(file); + let content = data.toString(); + fileContents.push(content); + }); + + let worker = Elm.Main.worker({ + "sources": fileContents, + "operation": "genswitch", + "languages": argv.language.split(","), + "format": argv.format, + }); + + let importDir = path.join(currentDir, argv.importOutput, argv.root); worker.ports.importResult.subscribe(function(resultString) { - handleImport(resultString); + handleImport(resultString, importDir); }); } @@ -104,16 +133,17 @@ function handleExport(resultString) { * * @param {[[String]]} results A list of (module name, file content) tuples. */ -function handleImport(results) { - let importDir = path.join(currentDir, argv.importOutput, argv.root, argv.language); +function handleImport(results, importDir) { fs.ensureDirSync(importDir); console.log("Writing elm-files files to:", importDir); results.forEach(function(result) { let moduleName = result[0]; if (moduleName.indexOf(argv.root) === 0) { - moduleName = moduleName.substr(argv.root.length + 1) + moduleName = moduleName.substr(argv.root.length + 1); } let filePath = path.join(importDir, moduleName + ".elm"); + // we also generate the top-level Translation.elm + filePath = filePath.replace(/\/(\.elm)$/, "$1"); fs.ensureDirSync(path.dirname(filePath)); fs.writeFileSync(filePath, result[1]); console.log("└── Finished writing:", filePath); diff --git a/src/CSV/Export.elm b/src/CSV/Export.elm index f6a471d..30d639b 100644 --- a/src/CSV/Export.elm +++ b/src/CSV/Export.elm @@ -4,6 +4,7 @@ module CSV.Export exposing (generate) (Localized.Element). @docs generate + -} import CSV.Template diff --git a/src/CSV/Import.elm b/src/CSV/Import.elm index 8248046..8020a50 100644 --- a/src/CSV/Import.elm +++ b/src/CSV/Import.elm @@ -29,7 +29,7 @@ You will usually use this output to create elm code: |> Localized.Writer.write -} -generate : String -> List ( String, List Localized.Element ) +generate : String -> List Localized.Module generate csv = case Csv.parse csv of Result.Ok lines -> @@ -40,7 +40,7 @@ generate csv = |> always [] -generateForCsv : Csv.Csv -> List ( String, List Localized.Element ) +generateForCsv : Csv.Csv -> List Localized.Module generateForCsv lines = let modules = @@ -60,18 +60,18 @@ generateForCsv lines = ) |> Dict.fromList in - -- Generate the source code for each module based on the lines - -- grouped in the expression above. - List.map - (\name -> - let - linesForThisModule = - Dict.get name linesForModules - |> Maybe.withDefault [] - in - ( name, generateForModule linesForThisModule ) - ) - modules + -- Generate the source code for each module based on the lines + -- grouped in the expression above. + List.map + (\name -> + let + linesForThisModule = + Dict.get name linesForModules + |> Maybe.withDefault [] + in + ( name, generateForModule linesForThisModule ) + ) + modules generateForModule : List (List String) -> List Localized.Element @@ -94,7 +94,7 @@ moduleNameForLine columns = Nothing -linesForModule : String -> List (List String) -> List (List String) +linesForModule : Localized.ModuleName -> List (List String) -> List (List String) linesForModule moduleName lines = List.filter (\line -> moduleNameForLine line == Just moduleName) lines @@ -109,7 +109,7 @@ fromLine columns = Nothing -code : String -> String -> String -> String -> String -> Localized.Element +code : Localized.ModuleName -> Localized.Key -> Localized.Comment -> String -> Localized.Value -> Localized.Element code modulename key comment placeholderString value = let placeholders = @@ -120,13 +120,13 @@ code modulename key comment placeholderString value = numPlaceholders = List.length placeholders in - if numPlaceholders == 0 then - staticElement modulename key comment value - else - formatElement modulename key comment placeholders value + if numPlaceholders == 0 then + staticElement modulename key comment value + else + formatElement modulename key comment placeholders value -formatElement : String -> String -> String -> List String -> String -> Localized.Element +formatElement : Localized.ModuleName -> Localized.Key -> Localized.Comment -> List Localized.Placeholder -> Localized.Value -> Localized.Element formatElement modulename key comment placeholders value = let components = @@ -139,7 +139,7 @@ formatElement modulename key comment placeholders value = -- "p}} Goodbye " -> ["p", " Goodbye "] String.split "}}" candidate |> withoutEmptyStrings - -- ["p", " Goodbye "] -> [FormatComponentPlaceholder "p", FormatComponentStatic " Goodbye "] + -- ["p", " Goodbye "] -> [FormatComponentPlaceholder "p", Localized.FormatComponentStatic " Goodbye "] |> List.indexedMap (\index submatch -> if index % 2 == 0 then @@ -152,18 +152,18 @@ formatElement modulename key comment placeholders value = ) |> List.concat in - Localized.ElementFormat - { meta = - { moduleName = modulename - , key = key - , comment = comment + Localized.ElementFormat + { meta = + { moduleName = modulename + , key = key + , comment = comment + } + , placeholders = placeholders + , components = components } - , placeholders = placeholders - , components = components - } -staticElement : String -> String -> String -> String -> Localized.Element +staticElement : Localized.ModuleName -> Localized.Key -> Localized.Comment -> Localized.Value -> Localized.Element staticElement modulename key comment value = Localized.ElementStatic { meta = diff --git a/src/Localized.elm b/src/Localized.elm index 85ed36d..a0a620d 100644 --- a/src/Localized.elm +++ b/src/Localized.elm @@ -1,17 +1,34 @@ module Localized exposing - ( Element(..) + ( Element(ElementStatic, ElementFormat) , Format - , FormatComponent(..) + , FormatComponent + ( FormatComponentStatic + , FormatComponentPlaceholder + ) , Meta , Static + , ModuleName + , Key + , Comment + , Value + , SourceCode + , Placeholder + , LangCode + , Module + , ModuleImplementation + , elementMeta + , languageModuleName , isEmptyFormatComponent + , elementRemoveLang + , namedModule ) {-| This module provides data structures describing localized string functions and constants. -@docs Element, Meta, Static, Format, FormatComponent, isEmptyFormatComponent +@docs Element, Meta, Static, Format, FormatComponent, ModuleName, Key, Comment, Value, Placeholder, Module, ModuleImplementation, SourceCode, LangCode, isEmptyFormatComponent, elementMeta, languageModuleName, elementRemoveLang, namedModule + -} @@ -23,14 +40,50 @@ type Element | ElementFormat Format +{-| The name of an Elm module. +-} +type alias ModuleName = + String + + +{-| A Key. +-} +type alias Key = + String + + +{-| Elm code (snipped) that will be written to an .elm file. +-} +type alias SourceCode = + String + + +{-| String representation of a human readable comment. +-} +type alias Comment = + String + + +{-| A String holding the final value of a static translation. +-} +type alias Value = + String + + +{-| A Placeholder represents one argument given to the Translation functions +-} +type alias Placeholder = + String + + {-| Each localized element (static or format) has a key that is unique within a module. The comment should help translators and others understand how and where the localized element is used. -} type alias Meta = - { moduleName : String - , key : String - , comment : String + { moduleName : ModuleName + , key : Key + , comment : Comment } @@ -38,7 +91,9 @@ type alias Meta = It contains a single string value. -} type alias Static = - { meta : Meta, value : String } + { meta : Meta + , value : Value + } {-| A formatted string can contain placeholders and static components. This @@ -52,14 +107,27 @@ allows us to describe strings that contain dynamic values. , FormatComponentPlaceholder "name" ] } + -} type alias Format = { meta : Meta - , placeholders : List String + , placeholders : List Placeholder , components : List FormatComponent } +{-| The representation of an Elm module containing a list of Elements. +-} +type alias Module = + ( ModuleName, List Element ) + + +{-| The representation of an Elm module with its complete source +-} +type alias ModuleImplementation = + ( ModuleName, SourceCode ) + + {-| A list of components make up a formatted element. See Format. -} type FormatComponent @@ -67,6 +135,12 @@ type FormatComponent | FormatComponentPlaceholder String +{-| A language code as a String used for module names, ie. "En" or "De" +-} +type alias LangCode = + String + + {-| Returns true if the component is empty. -} isEmptyFormatComponent : FormatComponent -> Bool @@ -77,3 +151,53 @@ isEmptyFormatComponent comp = FormatComponentPlaceholder string -> String.isEmpty string + + +{-| Returns an attribute of any Element +-} +elementMeta : (Meta -> val) -> Element -> val +elementMeta accessor element = + case element of + ElementStatic e -> + accessor e.meta + + ElementFormat e -> + accessor e.meta + + +{-| Returns an empty Module with a name indicating the locale. +-} +languageModuleName : ModuleName -> LangCode -> ModuleName +languageModuleName name lang = + name ++ "." ++ lang + + +{-| Constructs a new Module with the given name +-} +namedModule : ModuleName -> Module +namedModule name = + ( name, [] ) + + +{-| Removes a locale "En" from a module name like "Translation.Main.En" +-} +elementRemoveLang : LangCode -> Element -> Element +elementRemoveLang lang element = + let + moduleName = + elementMeta .moduleName element + + cleanedName = + String.split "." moduleName + |> List.filter (\p -> p /= lang) + |> String.join "." + + changeName meta name = + { meta | moduleName = name } + in + case element of + ElementStatic e -> + ElementStatic { e | meta = changeName e.meta cleanedName } + + ElementFormat e -> + ElementFormat { e | meta = changeName e.meta cleanedName } diff --git a/src/Localized/Parser.elm b/src/Localized/Parser.elm index f9f9a6c..b1bc48a 100644 --- a/src/Localized/Parser.elm +++ b/src/Localized/Parser.elm @@ -3,6 +3,7 @@ module Localized.Parser exposing (parse) {-| The parser parses elm code (one module) into a list of localized elements. @docs parse + -} import Localized @@ -12,7 +13,7 @@ import Localized.Parser.Internal exposing (..) {-| Parses the source code of an elm module and returns a list of localized elements. -} -parse : String -> List Localized.Element +parse : Localized.SourceCode -> List Localized.Element parse source = let stringKeysAndParameters = diff --git a/src/Localized/Parser/Internal.elm b/src/Localized/Parser/Internal.elm index 9068de1..7930bd4 100644 --- a/src/Localized/Parser/Internal.elm +++ b/src/Localized/Parser/Internal.elm @@ -33,7 +33,7 @@ regexFormats key = |> Regex.regex -findModuleName : String -> String +findModuleName : Localized.SourceCode -> Localized.ModuleName findModuleName source = Regex.find (Regex.AtMost 1) regexFindModuleName source |> List.head @@ -44,7 +44,7 @@ findModuleName source = {-| Finds all top level string declarations, both constants (`key : String` and functions returning strings (e.g. `fun : String -> String`). -} -stringDeclarations : String -> List ( String, List String ) +stringDeclarations : Localized.SourceCode -> List ( Localized.Key, List String ) stringDeclarations source = Regex.find Regex.All regexStringDeclarations source |> List.filterMap @@ -65,7 +65,7 @@ stringDeclarations source = ) -findStaticElementForKey : String -> String -> String -> Maybe Localized.Element +findStaticElementForKey : Localized.ModuleName -> Localized.SourceCode -> Localized.Key -> Maybe Localized.Element findStaticElementForKey moduleName source key = let maybeValue = @@ -83,7 +83,7 @@ findStaticElementForKey moduleName source key = Nothing -findFormatElementForKey : String -> String -> String -> Maybe Localized.Element +findFormatElementForKey : Localized.ModuleName -> Localized.SourceCode -> Localized.Key -> Maybe Localized.Element findFormatElementForKey moduleName source key = let regex = @@ -122,7 +122,7 @@ findFormatElementForKey moduleName source key = |> Just -findComment : String -> String -> String +findComment : Localized.SourceCode -> Localized.Key -> Localized.Comment findComment source key = let match = diff --git a/src/Localized/Switch.elm b/src/Localized/Switch.elm new file mode 100644 index 0000000..08d83e5 --- /dev/null +++ b/src/Localized/Switch.elm @@ -0,0 +1,165 @@ +module Localized.Switch exposing (generate) + +{-| + + Reads in all the Translation.elm files and generates a master switch for + them. Elements only present in one of them will still be added. +-} + +import Dict exposing (Dict) +import Localized +import Localized.Parser as Parser +import Localized.Writer.Module +import Localized.Writer.Element exposing (tab) + + +generate : List Localized.LangCode -> List Localized.SourceCode -> List ( Localized.ModuleName, Localized.SourceCode ) +generate languages sources = + mainModule languages + :: (sources + |> List.map Parser.parse + |> flatten2D + |> List.map (removeLocale languages) + |> unique + |> indexBy (Localized.elementMeta .moduleName) + |> Dict.toList + |> List.map (switchSource languages) + ) + + +unique : List Localized.Element -> List Localized.Element +unique elements = + u elements [] + + +u : List Localized.Element -> List Localized.Element -> List Localized.Element +u list have = + case list of + e :: rest -> + if member e have then + u rest have + else + u rest (e :: have) + + [] -> + have + + +flatten2D : List (List a) -> List a +flatten2D list = + List.foldr (++) [] list + + +member : Localized.Element -> List Localized.Element -> Bool +member e list = + let + sameElement e1 e2 = + case ( e1, e2 ) of + ( Localized.ElementFormat _, Localized.ElementStatic _ ) -> + False + + ( Localized.ElementStatic _, Localized.ElementFormat _ ) -> + False + + ( Localized.ElementStatic m1, Localized.ElementStatic m2 ) -> + m1.meta.moduleName == m2.meta.moduleName && m1.meta.key == m2.meta.key + + ( Localized.ElementFormat m1, Localized.ElementFormat m2 ) -> + m1.meta.moduleName == m2.meta.moduleName && m1.meta.key == m2.meta.key + in + List.any (sameElement e) list + + +indexBy : (Localized.Element -> comparable) -> List Localized.Element -> Dict comparable (List Localized.Element) +indexBy keymaker elements = + elements + |> List.foldr + (\e d -> + Dict.update (keymaker e) + (\v -> + case v of + Nothing -> + Just [ e ] + + Just l -> + Just (e :: l) + ) + d + ) + Dict.empty + + +switchSource : List Localized.LangCode -> Localized.Module -> ( Localized.ModuleName, Localized.SourceCode ) +switchSource languages mod = + let + ( moduleName, _ ) = + mod + in + ( moduleName + , Localized.Writer.Module.head mod + ++ Localized.Writer.Module.importModuleExposingAll ( "Translation", [] ) + ++ (String.join "" <| + List.map + (Localized.Writer.Module.importModule << Localized.namedModule << Localized.languageModuleName moduleName) + languages + ) + ++ "\n\n" + ++ Localized.Writer.Module.elements (elementSource languages) mod + ) + + +elementSource : List Localized.LangCode -> Localized.Element -> Localized.SourceCode +elementSource languages element = + let + name = + Localized.elementMeta .key element + + moduleName = + Localized.elementMeta .moduleName element + + placeholders = + Localized.Writer.Element.placeholders element + in + name + ++ " : Language -> " + ++ placeholders + ++ "\n" + ++ name + ++ " language =\n" + ++ (tab ++ "case language of\n") + ++ (String.join "\n" <| + List.map + (\l -> + (tab ++ tab) + ++ l + ++ " -> " + ++ moduleName + ++ "." + ++ l + ++ "." + ++ name + ) + languages + ) + + +mainModule : List Localized.LangCode -> ( Localized.ModuleName, Localized.SourceCode ) +mainModule languages = + let + name = + "Translation" + + mod = + ( name, [] ) + in + ( name + , Localized.Writer.Module.head mod + ++ "type Language = " + ++ String.join " | " languages + ++ "\n" + ) + + +removeLocale : List Localized.LangCode -> Localized.Element -> Localized.Element +removeLocale langs element = + langs |> List.foldr Localized.elementRemoveLang element diff --git a/src/Localized/Writer.elm b/src/Localized/Writer.elm index bebfa28..e53c820 100644 --- a/src/Localized/Writer.elm +++ b/src/Localized/Writer.elm @@ -5,106 +5,48 @@ module names and associated localized elements and returns the source code for elm modules implementing the localized elements. @docs generate + -} import Localized +import Localized.Writer.Module +import Localized.Writer.Element {-| Generate elm-source code for a list of modules and their associated localized elements. -} -generate : List ( String, List Localized.Element ) -> List ( String, String ) +generate : List Localized.Module -> List ( Localized.ModuleName, Localized.SourceCode ) generate = - List.map - (\( modulename, elements ) -> - ( modulename, moduleImplementation modulename elements ) - ) - - -moduleImplementation : String -> List Localized.Element -> String -moduleImplementation name elements = - "module " - ++ name - ++ " exposing (..)\n\n{-| -}\n\n\n" - ++ (List.map functionFromElement elements - |> String.join "\n\n\n" - |> String.trim - |> flip String.append "\n" - ) - - -functionFromElement : Localized.Element -> String -functionFromElement element = - case element of - Localized.ElementStatic static -> - functionStatic static - - Localized.ElementFormat format -> - functionFormat format - - -tab : String -tab = - " " + List.map moduleImplementation -functionStatic : Localized.Static -> String -functionStatic staticLocalized = - comment staticLocalized.meta.comment - ++ signature staticLocalized.meta.key [] - ++ ("\n" ++ tab ++ toString staticLocalized.value) - - -functionFormat : Localized.Format -> String -functionFormat format = - comment format.meta.comment - ++ signature format.meta.key format.placeholders - ++ "\n" - ++ (List.indexedMap formatComponentsImplementation format.components - |> String.join "\n" - ) - - -formatComponentsImplementation : Int -> Localized.FormatComponent -> String -formatComponentsImplementation index component = +moduleImplementation : Localized.Module -> ( Localized.ModuleName, Localized.SourceCode ) +moduleImplementation mod = let - prefix = - if index == 0 then - tab - else - tab ++ tab ++ "++ " + ( moduleName, _ ) = + mod in - case component of - Localized.FormatComponentStatic string -> - prefix ++ toString string - - Localized.FormatComponentPlaceholder string -> - prefix ++ String.trim string + ( moduleName + , Localized.Writer.Module.implementation element mod + ) -signature : String -> List String -> String -signature key placeholders = +element : Localized.Element -> Localized.SourceCode +element element = let - num = - List.length placeholders - - types = - if num == 0 then - "String" - else - String.join " -> " (List.repeat (num + 1) "String") - - parameters = - if num == 0 then - "" - else - " " ++ String.join " " placeholders + c = + Localized.elementMeta .comment element in - (key ++ " : " ++ types ++ "\n") - ++ (key ++ parameters ++ " =") + comment c + ++ Localized.Writer.Element.typeDeclaration element + ++ "\n" + ++ Localized.Writer.Element.head element + ++ "\n" + ++ Localized.Writer.Element.body element -comment : String -> String +comment : Localized.Comment -> Localized.SourceCode comment string = if String.isEmpty string then "" diff --git a/src/Localized/Writer/Element.elm b/src/Localized/Writer/Element.elm new file mode 100644 index 0000000..56705c3 --- /dev/null +++ b/src/Localized/Writer/Element.elm @@ -0,0 +1,82 @@ +module Localized.Writer.Element + exposing + ( typeDeclaration + , placeholders + , body + , head + , tab + , formatComponentsImplementation + ) + +import Localized + + +{-| Returns types of an translation element, ie. "helloWord : String" +-} +typeDeclaration : Localized.Element -> Localized.SourceCode +typeDeclaration element = + (Localized.elementMeta .key element) + ++ " : " + ++ placeholders element + + +placeholders : Localized.Element -> Localized.SourceCode +placeholders element = + case element of + Localized.ElementStatic _ -> + "String" + + Localized.ElementFormat { placeholders } -> + let + num = + List.length placeholders + in + String.join " -> " (List.repeat (num + 1) "String") + + +body : Localized.Element -> Localized.SourceCode +body element = + case element of + Localized.ElementStatic static -> + (tab ++ toString static.value) + + Localized.ElementFormat format -> + (List.indexedMap formatComponentsImplementation format.components + |> String.join "\n" + ) + + +head : Localized.Element -> Localized.SourceCode +head element = + (Localized.elementMeta .key element) + ++ (case element of + Localized.ElementStatic _ -> + "" + + Localized.ElementFormat format -> + " " + ++ String.join "" format.placeholders + ) + ++ " =" + + +tab : Localized.SourceCode +tab = + " " + + +formatComponentsImplementation : Int -> Localized.FormatComponent -> Localized.SourceCode +formatComponentsImplementation index component = + let + prefix = + if index == 0 then + tab + else + tab ++ tab ++ "++ " + in + case component of + Localized.FormatComponentStatic string -> + prefix ++ toString string + + Localized.FormatComponentPlaceholder string -> + prefix ++ String.trim string diff --git a/src/Localized/Writer/Module.elm b/src/Localized/Writer/Module.elm new file mode 100644 index 0000000..5193726 --- /dev/null +++ b/src/Localized/Writer/Module.elm @@ -0,0 +1,49 @@ +module Localized.Writer.Module + exposing + ( implementation + , elements + , head + , importModule + , importModuleExposingAll + ) + +{-| Provides code generation for Localized.Modules +-} + +import Localized + + +{-| Return the complete implementation for the Localized.Module, needs a function to implement each Localized.Element. +-} +implementation : (Localized.Element -> Localized.SourceCode) -> Localized.Module -> Localized.SourceCode +implementation functionImplementation mod = + head mod + ++ "\n" + ++ elements functionImplementation mod + + +elements : (Localized.Element -> Localized.SourceCode) -> Localized.Module -> Localized.SourceCode +elements functionImplementation ( _, elements ) = + List.map functionImplementation elements + |> String.join "\n\n\n" + |> String.trim + |> flip String.append "\n" + + +head : Localized.Module -> Localized.SourceCode +head ( name, _ ) = + "module " + ++ name + ++ " exposing (..)\n\n{-| -}\n\n" + + +importModule : Localized.Module -> Localized.SourceCode +importModule ( name, _ ) = + "import " ++ name ++ "\n" + + +importModuleExposingAll : Localized.Module -> Localized.SourceCode +importModuleExposingAll ( name, _ ) = + "import " + ++ name + ++ " exposing (..)\n" diff --git a/src/Main.elm b/src/Main.elm index 16a80d1..ba1e75c 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -4,13 +4,16 @@ port module Main exposing (main) in the CSV and PO submodules. @docs main + -} import CSV.Export import CSV.Import import Json.Decode +import Localized import Localized.Parser as Localized import Localized.Writer +import Localized.Switch import PO.Export import PO.Import import Platform exposing (programWithFlags) @@ -28,6 +31,7 @@ type Format type Operation = Export Format | Import Format + | GenSwitch Format port exportResult : String -> Cmd msg @@ -39,10 +43,15 @@ port importResult : List ( String, String ) -> Cmd msg operationFromString : String -> Maybe String -> Operation operationFromString operation formatString = formatFromString formatString - |> if operation == "import" then - Import - else - Export + |> case operation of + "import" -> + Import + + "export" -> + Export + + _ -> + GenSwitch formatFromString : Maybe String -> Format @@ -61,6 +70,7 @@ type alias Flags = { sources : List String , operation : String , format : Maybe String + , languages : Maybe (List String) } @@ -79,7 +89,10 @@ init flags = ( {}, operationExport flags.sources format ) Import format -> - ( {}, operationImport flags.sources format ) + ( {}, operationImport flags.sources flags.languages format ) + + GenSwitch _ -> + ( {}, operationGenerateSwitch flags.sources flags.languages ) operationExport : List String -> Format -> Cmd Never @@ -101,9 +114,12 @@ operationExport source format = exportResult exportValue -operationImport : List String -> Format -> Cmd Never -operationImport csv format = +operationImport : List String -> Maybe (List Localized.LangCode) -> Format -> Cmd Never +operationImport csv mlangs format = let + lang = + mlangs |> Maybe.withDefault [] |> List.head |> Maybe.withDefault "Klingon" + importFunction = case format of CSV -> @@ -115,11 +131,33 @@ operationImport csv format = List.head csv |> Maybe.withDefault "" |> importFunction + |> List.map (addLanguageToModuleName lang) |> Localized.Writer.generate - |> List.map (Tuple.mapFirst (String.split "." >> String.join "/")) + |> List.map slashifyModuleName + |> importResult + + +operationGenerateSwitch : List Localized.SourceCode -> Maybe (List Localized.LangCode) -> Cmd Never +operationGenerateSwitch sources mlangs = + let + locales = + Maybe.withDefault [] mlangs + in + Localized.Switch.generate locales sources + |> List.map slashifyModuleName |> importResult update : Never -> Model -> ( Model, Cmd Never ) update _ model = ( model, Cmd.none ) + + +slashifyModuleName : Localized.ModuleImplementation -> Localized.ModuleImplementation +slashifyModuleName = + Tuple.mapFirst (String.split "." >> String.join "/") + + +addLanguageToModuleName : Localized.LangCode -> Localized.Module -> Localized.Module +addLanguageToModuleName lang = + Tuple.mapFirst (flip Localized.languageModuleName lang) diff --git a/src/PO/Export.elm b/src/PO/Export.elm index b4807cf..37d1940 100644 --- a/src/PO/Export.elm +++ b/src/PO/Export.elm @@ -1,10 +1,11 @@ module PO.Export exposing (generate) {-| The PO export generates PO strings from a list of localized elements -(Localized.Element). For more information about the PO Format visit: -https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html +(Localized.Element). For more information about the PO Localized.Format visit: + @docs generate + -} import Localized @@ -46,7 +47,7 @@ line element = ++ ("msgstr " ++ formatElement format.components) -commentLine : String -> String +commentLine : Localized.Comment -> String commentLine comment = String.split "\n" comment |> String.join "\n#. " @@ -54,12 +55,12 @@ commentLine comment = |> String.trim -identifier : String -> String -> String +identifier : Localized.ModuleName -> Localized.Key -> String identifier modulename key = "msgid \"" ++ modulename ++ "." ++ key ++ "\"" -staticElement : String -> String +staticElement : Localized.Value -> String staticElement value = "msgstr " ++ toString value diff --git a/src/PO/Import.elm b/src/PO/Import.elm index 49aa798..1148b84 100644 --- a/src/PO/Import.elm +++ b/src/PO/Import.elm @@ -9,10 +9,11 @@ Use Localized.Writer.write to create elm code from the list of localized elements. @docs generate + -} import Dict exposing (Dict) -import Localized exposing (FormatComponent) +import Localized import PO.Import.Internal exposing (..) @@ -27,7 +28,7 @@ You will usually use this output to create elm code: |> Localized.Writer.write -} -generate : String -> List ( String, List Localized.Element ) +generate : String -> List Localized.Module generate poString = let keysInModules = @@ -41,7 +42,7 @@ generate poString = keysInModules -generateModule : String -> String -> List String -> List Localized.Element +generateModule : String -> Localized.ModuleName -> List Localized.Key -> List Localized.Element generateModule poString moduleName allKeys = let fullComments = diff --git a/src/PO/Import/Internal.elm b/src/PO/Import/Internal.elm index 5424596..1485ee3 100644 --- a/src/PO/Import/Internal.elm +++ b/src/PO/Import/Internal.elm @@ -9,7 +9,7 @@ module PO.Import.Internal ) import Dict exposing (Dict) -import Localized exposing (FormatComponent) +import Localized import Regex exposing (Regex) import Set import String.Extra as String @@ -17,7 +17,7 @@ import Utils.Regex import PO.Template -element : String -> String -> String -> String -> Localized.Element +element : Localized.ModuleName -> Localized.Key -> Localized.Value -> Localized.Comment -> Localized.Element element moduleName key value fullComment = let comment = @@ -65,7 +65,7 @@ element moduleName key value fullComment = ---- KEYS -fullKey : String -> String -> String +fullKey : Localized.ModuleName -> Localized.Key -> String fullKey moduleName key = moduleName ++ "." ++ key @@ -73,7 +73,7 @@ fullKey moduleName key = {-| Extract a list of all localization keys per module from a PO string file's contents. -} -keys : String -> List ( String, List String ) +keys : String -> List ( Localized.ModuleName, List String ) keys poString = let matches = @@ -119,7 +119,7 @@ keys poString = file, for all given keys in a module. The dict will contain use the localized key for key and the value will be the multiline comment. -} -poComments : String -> String -> List String -> Dict String String +poComments : String -> Localized.ModuleName -> List Localized.Key -> Dict Localized.Key String poComments poString moduleName allKeys = allKeys |> List.map @@ -133,7 +133,7 @@ poComments poString moduleName allKeys = |> Dict.fromList -commentFromPoComment : String -> String +commentFromPoComment : String -> Localized.Comment commentFromPoComment poComment = String.trim poComment |> String.split "#." @@ -153,8 +153,9 @@ for our own exports where we write placeholders into the comment using the following format: #. i18n: placeholders: placeh1, placeh2 + -} -placeholdersFromPoComment : String -> List String +placeholdersFromPoComment : String -> List Localized.Placeholder placeholdersFromPoComment poComment = let placeholdersPrefix = @@ -184,7 +185,7 @@ placeholdersFromPoComment poComment = {-| Extract all values for a module and a given list of keys from a PO file. The dict will reference the value by its localization key. -} -values : String -> String -> List String -> Dict String String +values : String -> Localized.ModuleName -> List Localized.Key -> Dict Localized.Key Localized.Value values poString moduleName allKeys = allKeys |> List.map @@ -203,18 +204,18 @@ values poString moduleName allKeys = placeholder definition is missing form the comment, but ma lead to issues with sortine. For this reason, we only use this as a fallback an log an error. -} -placeholdersInValue : String -> List String +placeholdersInValue : Localized.Value -> List Localized.Placeholder placeholdersInValue value = Regex.find Regex.All regexForPlaceholder value |> List.filterMap (\match -> Utils.Regex.submatchAt 0 (Just match)) -formatComponentsFromValue : String -> List String -> List Localized.FormatComponent +formatComponentsFromValue : Localized.Value -> List Localized.Placeholder -> List Localized.FormatComponent formatComponentsFromValue value placeholders = findPlaceholdersInStaticComponents [ Localized.FormatComponentStatic value ] placeholders -findPlaceholdersInStaticComponents : List Localized.FormatComponent -> List String -> List Localized.FormatComponent +findPlaceholdersInStaticComponents : List Localized.FormatComponent -> List Localized.Placeholder -> List Localized.FormatComponent findPlaceholdersInStaticComponents components placeholders = case List.head placeholders of Nothing -> @@ -249,13 +250,13 @@ findPlaceholdersInStaticComponents components placeholders = ---- REGEX -regexComments : String -> Regex +regexComments : Localized.Key -> Regex regexComments key = -- Find all lines preceeding `msgid "key"` that start with `#.` Regex.regex ("((?:#\\.[^\\n]*\\n)*)msgid " ++ toString key) -regexForValue : String -> Regex +regexForValue : Localized.Key -> Regex regexForValue key = -- Find all lines succeeding `msgid "key" \nmsgstr` until the two successive white lines Regex.regex