Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/default ts translations #38

Merged
merged 13 commits into from May 7, 2019
@@ -4,6 +4,7 @@ version 0.5.0
- adds option to UniferConfiguration that allows to change the way how request extractors are found.
- adds option to pass component's defaultConfiguration a function that returns a new configuration object
- passes list of components to except from autobind to inversify-components
- loads all TypeScript, JavaScript and JSON files from locales directory, if not i18next backend is configured

This comment has been minimized.

Copy link
@antoniusostermann

antoniusostermann Apr 18, 2019

Collaborator

Could you please add some more information about the ordering of loading and all possibilites (one file per state, on file per intent, folders, ...). Since we don't have docs currently, that info would be probably lost otherwise.


version 0.4.1
- fixes typing of StayInContextCallback and ClearContextCallback

Some generated files are not rendered by default. Learn more.

@@ -47,6 +47,7 @@
"lodash": "^4.17.11",
"redis": "2.8.0",
"reflect-metadata": "^0.1.12",
"require-directory": "^2.1.1",
"resolve": "^1.8.1",
"rxjs": "^5.5.11"
},
@@ -17,10 +17,12 @@ const configuration: AssistantJSConfiguration = {
"core:i18n": {
// This is basically the i18next configuration. Check out https://www.i18next.com/ for more information!
i18nextAdditionalConfiguration: {
// This entry is needed and tells i18next where to find your language files.
backend: {
loadPath: process.cwd() + "/config/locales/{{lng}}/{{ns}}.json",
},
// Translation resources are automatically loaded from path given in `UnifierConfiguration#utterancePath`, but may be overridden here.
// resources: {}
// Alternatively you can give a path to a folder with JSON files that can be loaded by i18next
// backend: {
// loadPath: process.cwd() + "/config/locales/{{lng}}/{{ns}}.json",
// },
lngs: ["en"],
fallbackLng: "en",
// If you encouter problems with i18next, change this to true
@@ -1,8 +1,11 @@
import * as path from "path";

import { TEMPORARY_INTERPOLATION_END, TEMPORARY_INTERPOLATION_START } from "../../../src/components/i18n/interpolation-resolver";
import { arraySplitter } from "../../../src/components/i18n/plugins/array-returns-sample.plugin";
import { I18nextWrapper } from "../../../src/components/i18n/wrapper";
import { injectionNames } from "../../../src/injection-names";
import { configureI18nLocale } from "../../support/util/i18n-configuration";
import { configureUnifier } from "../../support/util/unifier-configuration";
import { ThisContext } from "../../this-context";

interface CurrentThisContext extends ThisContext {
@@ -56,3 +59,41 @@ describe("I18nWrapper", function() {
});
});
});

describe("I18nWrapper loading Typescript files", function(this: CurrentThisContext) {
const expectedTranslations = ["hello my name", "hi my name", "welcome my name"];

beforeEach(function(this: CurrentThisContext) {
this.specHelper.prepareSpec(this.defaultSpecOptions);
// Remove emitting of warnings
this.specHelper.bindSpecLogger("error");

configureI18nLocale(this.assistantJs.container, false);
configureUnifier(this.assistantJs.container, path.join(__dirname, "../../support/mocks/i18n-ts/locale/"));
this.wrapper = this.inversify.get("core:i18n:wrapper");
});

describe("translation function", function() {
it("returns one of many options", function(this: CurrentThisContext) {
expect(expectedTranslations).toContain(this.wrapper.instance.t("templateSyntaxSmall", { name: "my name" }));
});
});

describe("loading behaviour", function() {
it("load from file that has the same name as a directory", function() {
expect(this.wrapper.instance.store.data.de.translation.mySpecificKeys.keyOne).toBe("keyOneResult");
});

it("loads default only if exists", function() {
expect(this.wrapper.instance.store.data.de.translation.defaultExport.test).toBe("default");
});

it("loads export with camelcase filename if no default exists", function() {
expect(this.wrapper.instance.store.data.de.translation.templateSyntaxSmall[0]).toBe("{hello|hi} {{name}}");
});

it("loads all exports if neither default nor one equal to camelcase filename exists", function() {
expect(this.wrapper.instance.store.data.de.translation.mainState.testIntent.embedded.test).toBe("very-specific-without-extractor");
});
});
});
@@ -0,0 +1,56 @@
export default {
mySpecificKeys: {
keyOne: "keyOneResult",
},
multiple: ["a", "b"],
var: "a{{var}}",
multipleVars: "a{{firstVar}}b{{secondVar}}c{{thirdVar}}",
deviceDependentState: {
ExtractorComponent: {
device1: "state-platform-device-specific",
},
},
root: {
testIntent: {
embedded: {
platformDependent: {
ExtractorComponent: "platform-specific-embedded",
},
platformIndependent: "platform-independent-root-embedded",
},
withoutExtractor: "root-without-extractor",
deviceDependent: {
ExtractorComponent: {
device1: "root-intent-platform-device-specific",
},
},
},
secondPlatformSpecificIntent: {
ExtractorComponent: "root-platform-specific-intent",
},
yesGenericIntent: "root-yes",
rootKey: {
ExtractorComponent: "platform-specific-root-only",
},
ExtractorComponent: "root-only-platform-given",
},
noIntentState: "stateOnly",
templateSyntax: ["{Can|May} I help you, {{var}}?", "Would you like me to help you?"],
filter: {
stateA: {
intentA: "FilterAState - filterTestAIntent",
intentB: "FilterAState - filterTestBIntent",
intentC: "FilterAState - filterTestCIntent",
intentD: "FilterAState - filterTestDIntent",
},
stateB: {
intentA: "FilterBState - filterTestAIntent",
intentB: "FilterBState - filterTestBIntent",
},
stateC: {
intentA: "FilterCState - filterTestAIntent",
intentB: "FilterCState - filterTestBIntent",
},
},
noInterpolation: "no interpolation",
};
@@ -0,0 +1,2 @@
export const someState = { some: "state" };
export default { test: "default" };
@@ -0,0 +1,32 @@
export const testIntent = {
embedded: {
test: "very-specific-without-extractor",
platformDependent: {
ExtractorComponent: "platform-specific-sub-key",
},
},
};

export const deviceDependentIntent = {
embeddedKeyOuter: {
embeddedKeyInner: {
ExtractorComponent: {
device1: "device-specific-sub-key",
},
},
},
};

export const yesGenericIntent = "yes";

export const platformDependent = {
ExtractorComponent: "platform-specific-embedded-state-only",
};

export const platformIndependent = "platform-independent-main-state";

export const ExtractorComponent = "platform-specific-main-state-only";

export const platformSpecificIntent = {
ExtractorComponent: "platform-specific-intent",
};
@@ -0,0 +1 @@
export const templateSyntaxSmall = ["{hello|hi} {{name}}", "welcome {{name}}"];
@@ -27,4 +27,12 @@ export class LocalesLoaderMock implements LocalesLoader {
},
};
}

public getTranslations() {
return {};
}

public getLocales() {
return undefined;
}
}
@@ -0,0 +1,11 @@
import * as i18next from "i18next";
import { Container } from "inversify-components";
import { UnifierConfiguration } from "../../../src/assistant-source";

export function configureUnifier(container: Container, utterancePath: string) {
const config: UnifierConfiguration = {
utterancePath,
};

container.componentRegistry.lookup("core:unifier").addConfiguration(config);
}
@@ -10,6 +10,7 @@ import * as commander from "commander";
// Import node.js filesystem module
import * as fs from "fs";
import { Component } from "inversify-components";
import * as path from "path";
import { AssistantJSApplicationInitializer } from "./components/joined-interfaces";

// Get package.json data
@@ -63,6 +64,8 @@ export function cli(argv, resolvedApplicationInitializer) {
"config",
"config/locales",
"config/locales/en",
"config/locales/en/translation",
"config/locales/en/utterances",
"spec",
"spec/app",
"spec/app/states",
@@ -104,10 +107,16 @@ export function cli(argv, resolvedApplicationInitializer) {
fs.closeSync(fs.openSync(projectPath + touchableFile, "w"));
});

// Create empty json files
["config/locales/en/translation.json", "config/locales/en/utterances.json"].forEach(filePath => {
// Create base TypesScript files
[
{ filePath: "config/locales/en/translation/main-state.ts", exportString: "{}" },
{ filePath: "config/locales/en/utterances/invokeGenericIntent.ts", exportString: "[]" },

This comment has been minimized.

Copy link
@antoniusostermann

antoniusostermann Apr 18, 2019

Collaborator

please align this file path to our regular conventions. You should not use camelCase in path strings ("invoke-generic-intent.ts" instead of "invokeGenericIntent.ts").

].forEach(({ filePath, exportString }) => {
console.log("Creating " + filePath + "..");
fs.writeFileSync(projectPath + filePath, "{}");
fs.writeFileSync(
projectPath + filePath,
`export const ${path.basename(filePath, path.extname(filePath)).replace(/[\-]+([a-z])/gi, (m, c) => c.toUpperCase())} = ${exportString};`
);
});
};

@@ -5,6 +5,7 @@ import { arraySplitter } from "./plugins/array-returns-sample.plugin";
import { Configuration } from "./private-interfaces";

import { injectionNames, Logger } from "../../assistant-source";
import { LocalesLoader } from "../unifier/public-interfaces";
import { I18nContext } from "./context";
import { InterpolationResolver as InterpolationResolverImpl } from "./interpolation-resolver";
import { InterpolationResolver, TranslateHelper, TranslateHelperFactory, TranslateValuesFor } from "./public-interfaces";
@@ -37,6 +38,7 @@ export const descriptor: ComponentDescriptor<Configuration.Defaults> = {
return new I18nextWrapper(
context.container.get<Component<Configuration.Runtime>>(getMetaInjectionName("core:i18n")),
context.container.get<Logger>(injectionNames.logger),
context.container.get<LocalesLoader>(injectionNames.localesLoader),
false
);
})
@@ -1,12 +1,18 @@
import * as fs from "fs";
import * as path from "path";

import { Component, getMetaInjectionName } from "inversify-components";
import { Configuration } from "./private-interfaces";

import * as i18next from "i18next";
import * as i18nextBackend from "i18next-sync-fs-backend";
import { inject, injectable, multiInject, optional } from "inversify";
import { inject, injectable } from "inversify";
import { injectionNames } from "../../injection-names";
import { Logger } from "../root/public-interfaces";

import * as requireDir from "require-directory";

import { LocalesLoader } from "../unifier/public-interfaces";
import { TEMPORARY_INTERPOLATION_END, TEMPORARY_INTERPOLATION_START } from "./interpolation-resolver";
import { arraySplitter, processor } from "./plugins/array-returns-sample.plugin";
import { processor as templateProcessor } from "./plugins/parse-template-language.plugin";
@@ -25,6 +31,7 @@ export class I18nextWrapper {
constructor(
@inject(getMetaInjectionName("core:i18n")) componentMeta: Component<Configuration.Runtime>,
@inject(injectionNames.logger) logger: Logger,
@inject(injectionNames.localesLoader) localesLoader: LocalesLoader,
returnOnlySample = true
) {
this.component = componentMeta;
@@ -36,7 +43,10 @@ export class I18nextWrapper {
}

this.instance = Object.assign(Object.create(Object.getPrototypeOf(this.configuration.i18nextInstance)), this.configuration.i18nextInstance);

const i18nextConfiguration = {
// If locales are found in `UnifierConfiguration#utterancePath`, pre-populate resources with them. May be overridden by `I18NConfiguration#backend`.
...{ resources: localesLoader.getLocales() },
...{ initImmediate: false, missingInterpolationHandler: this.onInterpolationMissing.bind(this) },
...this.configuration.i18nextAdditionalConfiguration,
};
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.