@dittowords/cli 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/install-node-dependencies/action.yml +24 -0
- package/.github/workflows/required-checks.yml +24 -0
- package/__mocks__/fs.js +2 -0
- package/bin/__mocks__/api.js +48 -0
- package/bin/__mocks__/api.js.map +1 -0
- package/bin/config.js +6 -4
- package/bin/config.js.map +1 -1
- package/bin/config.test.js +4 -3
- package/bin/config.test.js.map +1 -1
- package/bin/consts.js +19 -29
- package/bin/consts.js.map +1 -1
- package/bin/generate-suggestions.js +68 -58
- package/bin/generate-suggestions.js.map +1 -1
- package/bin/generate-suggestions.test.js +24 -13
- package/bin/generate-suggestions.test.js.map +1 -1
- package/bin/http/__mocks__/fetchComponentFolders.js +71 -0
- package/bin/http/__mocks__/fetchComponentFolders.js.map +1 -0
- package/bin/http/__mocks__/fetchComponents.js +73 -0
- package/bin/http/__mocks__/fetchComponents.js.map +1 -0
- package/bin/http/__mocks__/fetchVariants.js +71 -0
- package/bin/http/__mocks__/fetchVariants.js.map +1 -0
- package/bin/http/fetchComponentFolders.js +4 -4
- package/bin/http/fetchComponentFolders.js.map +1 -1
- package/bin/http/fetchComponents.js +4 -4
- package/bin/http/fetchComponents.js.map +1 -1
- package/bin/http/fetchVariants.js +2 -2
- package/bin/http/fetchVariants.js.map +1 -1
- package/bin/http/http.test.js +159 -0
- package/bin/http/http.test.js.map +1 -0
- package/bin/http/importComponents.js +9 -2
- package/bin/http/importComponents.js.map +1 -1
- package/bin/init/project.test.js +5 -28
- package/bin/init/project.test.js.map +1 -1
- package/bin/init/token.js +72 -27
- package/bin/init/token.js.map +1 -1
- package/bin/init/token.test.js +87 -9
- package/bin/init/token.test.js.map +1 -1
- package/bin/pull-lib.test.js +379 -0
- package/bin/pull-lib.test.js.map +1 -0
- package/bin/pull.js +13 -5
- package/bin/pull.js.map +1 -1
- package/bin/pull.test.js +100 -289
- package/bin/pull.test.js.map +1 -1
- package/bin/replace.js +22 -6
- package/bin/replace.js.map +1 -1
- package/bin/replace.test.js +53 -11
- package/bin/replace.test.js.map +1 -1
- package/bin/types.js +2 -2
- package/bin/types.js.map +1 -1
- package/bin/utils/determineModuleType.js +6 -7
- package/bin/utils/determineModuleType.js.map +1 -1
- package/bin/utils/determineModuleType.test.js +60 -0
- package/bin/utils/determineModuleType.test.js.map +1 -0
- package/bin/utils/getSelectedProjects.js +5 -5
- package/bin/utils/getSelectedProjects.js.map +1 -1
- package/bin/utils/quit.js +3 -3
- package/bin/utils/quit.js.map +1 -1
- package/jest.config.ts +16 -0
- package/lib/__mocks__/api.ts +12 -0
- package/lib/config.test.ts +3 -1
- package/lib/config.ts +4 -2
- package/lib/consts.ts +19 -17
- package/lib/generate-suggestions.test.ts +23 -11
- package/lib/generate-suggestions.ts +89 -79
- package/lib/http/__mocks__/fetchComponentFolders.ts +23 -0
- package/lib/http/__mocks__/fetchComponents.ts +24 -0
- package/lib/http/__mocks__/fetchVariants.ts +21 -0
- package/lib/http/fetchComponentFolders.ts +6 -4
- package/lib/http/fetchComponents.ts +5 -3
- package/lib/http/fetchVariants.ts +14 -3
- package/lib/http/http.test.ts +122 -0
- package/lib/http/importComponents.ts +8 -0
- package/lib/init/project.test.ts +4 -27
- package/lib/init/token.test.ts +55 -7
- package/lib/init/token.ts +76 -27
- package/lib/pull-lib.test.ts +367 -0
- package/lib/pull.test.ts +97 -310
- package/lib/pull.ts +11 -3
- package/lib/replace.test.ts +46 -10
- package/lib/replace.ts +20 -3
- package/lib/types.ts +5 -0
- package/lib/utils/determineModuleType.test.ts +48 -0
- package/lib/utils/determineModuleType.ts +4 -6
- package/lib/utils/getSelectedProjects.ts +3 -3
- package/lib/utils/quit.ts +1 -1
- package/package.json +4 -3
- package/jest.config.js +0 -6
package/lib/init/token.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
2
3
|
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ import consts from "../consts";
|
|
|
9
10
|
import output from "../output";
|
|
10
11
|
import config from "../config";
|
|
11
12
|
import { quit } from "../utils/quit";
|
|
13
|
+
import { AxiosError, AxiosResponse } from "axios";
|
|
12
14
|
|
|
13
15
|
export const needsToken = (configFile?: string, host = consts.API_HOST) => {
|
|
14
16
|
if (config.getTokenFromEnv()) {
|
|
@@ -26,51 +28,82 @@ export const needsToken = (configFile?: string, host = consts.API_HOST) => {
|
|
|
26
28
|
return false;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
async function verifyTokenUsingTokenCheck(
|
|
32
|
+
token: string
|
|
33
|
+
): Promise<{ success: true } | { success: false; output: string[] }> {
|
|
31
34
|
const axios = createApiClient(token);
|
|
32
35
|
const endpoint = "/token-check";
|
|
33
36
|
|
|
34
|
-
let resOrError;
|
|
37
|
+
let resOrError: AxiosResponse<any> | undefined;
|
|
35
38
|
try {
|
|
36
|
-
resOrError = await axios.get(endpoint)
|
|
37
|
-
if (error.code === "ENOTFOUND") {
|
|
38
|
-
return output.errorText(
|
|
39
|
-
`Can't connect to API: ${output.url(error.hostname)}`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (error.response.status === 401 || error.response.status === 404) {
|
|
43
|
-
return output.errorText(
|
|
44
|
-
"This API key isn't valid. Please try another."
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
return output.warnText("We're having trouble reaching the Ditto API.");
|
|
48
|
-
});
|
|
39
|
+
resOrError = await axios.get(endpoint);
|
|
49
40
|
} catch (e: unknown) {
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
if (!(e instanceof AxiosError)) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
output: [
|
|
45
|
+
output.warnText(
|
|
46
|
+
"Sorry! We're having trouble reaching the Ditto API."
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (e.code === "ENOTFOUND") {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
output: [
|
|
56
|
+
output.errorText(
|
|
57
|
+
`Can't connect to API: ${output.url(consts.API_HOST)}`
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (e.response?.status === 401 || e.response?.status === 404) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
output: [
|
|
67
|
+
output.errorText("This API key isn't valid. Please try another."),
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
if (typeof resOrError === "string") {
|
|
55
|
-
return
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
output: [resOrError],
|
|
77
|
+
};
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
if (resOrError?.status === 200) {
|
|
59
|
-
return true;
|
|
81
|
+
return { success: true };
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
output: [output.errorText("This API key isn't valid. Please try another.")],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Returns true if valid, otherwise an error message.
|
|
91
|
+
async function checkToken(token: string): Promise<any> {
|
|
92
|
+
const result = await verifyTokenUsingTokenCheck(token);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
return result.output.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true;
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
async function collectToken(message: string | null) {
|
|
66
101
|
const blue = output.info;
|
|
67
|
-
const apiUrl = output.url("https://app.dittowords.com/account/
|
|
68
|
-
const breadcrumbs = `${blue("
|
|
102
|
+
const apiUrl = output.url("https://app.dittowords.com/account/devtools");
|
|
103
|
+
const breadcrumbs = `${chalk.bold(blue("API Keys"))}`;
|
|
69
104
|
const tokenDescription =
|
|
70
105
|
message ||
|
|
71
|
-
`To get started, you'll need your Ditto API key. You can find this at: ${apiUrl}
|
|
72
|
-
"API Keys"
|
|
73
|
-
)}".`;
|
|
106
|
+
`To get started, you'll need your Ditto API key. You can find this at: ${apiUrl} under "${breadcrumbs}".`;
|
|
74
107
|
console.log(tokenDescription);
|
|
75
108
|
|
|
76
109
|
const response = await prompt<{ token: string }>({
|
|
@@ -100,8 +133,24 @@ export const collectAndSaveToken = async (message: string | null = null) => {
|
|
|
100
133
|
config.saveToken(consts.CONFIG_FILE, consts.API_HOST, token);
|
|
101
134
|
return token;
|
|
102
135
|
} catch (error) {
|
|
103
|
-
|
|
136
|
+
// https://github.com/enquirer/enquirer/issues/225#issue-516043136
|
|
137
|
+
// Empty string corresponds to the user hitting Ctrl + C
|
|
138
|
+
if (error === "") {
|
|
139
|
+
quit("", 0);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const eventId = Sentry.captureException(error);
|
|
144
|
+
const eventStr = `\n\nError ID: ${output.info(eventId)}`;
|
|
145
|
+
|
|
146
|
+
return quit(
|
|
147
|
+
output.errorText(
|
|
148
|
+
"Something went wrong. Please contact support or try again later."
|
|
149
|
+
) + eventStr
|
|
150
|
+
);
|
|
104
151
|
}
|
|
105
152
|
};
|
|
106
153
|
|
|
107
154
|
export default { needsToken, collectAndSaveToken };
|
|
155
|
+
|
|
156
|
+
export const _test = { verifyTokenUsingTokenCheck };
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { _test } from "./pull";
|
|
4
|
+
import { jest } from "@jest/globals";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
const axiosMock = jest.mocked(axios);
|
|
7
|
+
|
|
8
|
+
jest.mock("fs");
|
|
9
|
+
jest.mock("./api");
|
|
10
|
+
|
|
11
|
+
const testProjects: Project[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "1",
|
|
14
|
+
name: "Project 1",
|
|
15
|
+
fileName: "Project 1",
|
|
16
|
+
},
|
|
17
|
+
{ id: "2", name: "Project 2", fileName: "Project 2" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
jest.mock("./consts", () => ({
|
|
21
|
+
TEXT_DIR: "/.testing",
|
|
22
|
+
API_HOST: "https://api.dittowords.com",
|
|
23
|
+
CONFIG_FILE: "/.testing/ditto",
|
|
24
|
+
PROJECT_CONFIG_FILE: "/.testing/config.yml",
|
|
25
|
+
TEXT_FILE: "/.testing/text.json",
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import consts from "./consts";
|
|
29
|
+
import allPull, { getFormatDataIsValid } from "./pull";
|
|
30
|
+
import { Project, SupportedExtension, SupportedFormat } from "./types";
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
_testing: { cleanOutputFiles, downloadAndSaveBase },
|
|
34
|
+
} = allPull;
|
|
35
|
+
|
|
36
|
+
const cleanOutputDir = () => {
|
|
37
|
+
if (fs.existsSync(consts.TEXT_DIR))
|
|
38
|
+
fs.rmSync(consts.TEXT_DIR, { recursive: true, force: true });
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(consts.TEXT_DIR);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
fs.rmSync(consts.TEXT_DIR, { force: true, recursive: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("cleanOutputFiles", () => {
|
|
48
|
+
it("removes .js, .json, .xml, .strings, .stringsdict files", () => {
|
|
49
|
+
cleanOutputDir();
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.json"), "test");
|
|
52
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.js"), "test");
|
|
53
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.xml"), "test");
|
|
54
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.strings"), "test");
|
|
55
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.stringsdict"), "test");
|
|
56
|
+
// this file shouldn't be deleted
|
|
57
|
+
fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.txt"), "test");
|
|
58
|
+
|
|
59
|
+
expect(fs.readdirSync(consts.TEXT_DIR).length).toEqual(6);
|
|
60
|
+
|
|
61
|
+
cleanOutputFiles();
|
|
62
|
+
|
|
63
|
+
expect(fs.readdirSync(consts.TEXT_DIR).length).toEqual(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("downloadAndSaveBase", () => {
|
|
68
|
+
beforeAll(() => {
|
|
69
|
+
if (!fs.existsSync(consts.TEXT_DIR)) {
|
|
70
|
+
fs.mkdirSync(consts.TEXT_DIR);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
cleanOutputDir();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mockDataFlat = { hello: "world" };
|
|
79
|
+
const mockDataNested = { hello: { text: "world" } };
|
|
80
|
+
const mockDataStructured = { hello: { text: "world" } };
|
|
81
|
+
const mockDataIcu = { hello: "world" };
|
|
82
|
+
const mockDataAndroid = `
|
|
83
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
84
|
+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
|
85
|
+
<string name="hello-world" ditto_api_id="hello-world">Hello World</string>
|
|
86
|
+
</resources>
|
|
87
|
+
`;
|
|
88
|
+
const mockDataIosStrings = `
|
|
89
|
+
"hello" = "world";
|
|
90
|
+
`;
|
|
91
|
+
const mockDataIosStringsDict = `
|
|
92
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
93
|
+
<plist version="1.0">
|
|
94
|
+
<dict>
|
|
95
|
+
<key>hello-world</key>
|
|
96
|
+
<dict>
|
|
97
|
+
<key>NSStringLocalizedFormatKey</key>
|
|
98
|
+
<string>%1$#@count@</string>
|
|
99
|
+
<key>count</key>
|
|
100
|
+
<dict>
|
|
101
|
+
<key>NSStringFormatSpecTypeKey</key>
|
|
102
|
+
<string>NSStringPluralRuleType</string>
|
|
103
|
+
<key>NSStringFormatValueTypeKey</key>
|
|
104
|
+
<string>d</string>
|
|
105
|
+
<key>other</key>
|
|
106
|
+
<string>espanol</string>
|
|
107
|
+
</dict>
|
|
108
|
+
</dict>
|
|
109
|
+
</dict>
|
|
110
|
+
</plist>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
const formats: Array<{
|
|
114
|
+
format: SupportedFormat;
|
|
115
|
+
data: Object;
|
|
116
|
+
ext: SupportedExtension;
|
|
117
|
+
}> = [
|
|
118
|
+
{ format: "flat", data: mockDataFlat, ext: ".json" },
|
|
119
|
+
{ format: "nested", data: mockDataNested, ext: ".json" },
|
|
120
|
+
{ format: "structured", data: mockDataStructured, ext: ".json" },
|
|
121
|
+
{ format: "icu", data: mockDataIcu, ext: ".json" },
|
|
122
|
+
{ format: "android", data: mockDataAndroid, ext: ".xml" },
|
|
123
|
+
{ format: "ios-strings", data: mockDataIosStrings, ext: ".strings" },
|
|
124
|
+
{
|
|
125
|
+
format: "ios-stringsdict",
|
|
126
|
+
data: mockDataIosStringsDict,
|
|
127
|
+
ext: ".stringsdict",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const mockApiCall = (data: unknown) => {
|
|
132
|
+
axiosMock.get.mockResolvedValue({ data });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const verifySavedData = async (
|
|
136
|
+
format: SupportedFormat,
|
|
137
|
+
data: unknown,
|
|
138
|
+
ext: SupportedExtension
|
|
139
|
+
) => {
|
|
140
|
+
const output = await downloadAndSaveBase({
|
|
141
|
+
projects: testProjects,
|
|
142
|
+
format: format,
|
|
143
|
+
} as any);
|
|
144
|
+
expect(/successfully saved/i.test(output)).toEqual(true);
|
|
145
|
+
const directoryContents = fs.readdirSync(consts.TEXT_DIR);
|
|
146
|
+
expect(directoryContents.length).toEqual(testProjects.length);
|
|
147
|
+
expect(directoryContents.every((f) => f.endsWith(ext))).toBe(true);
|
|
148
|
+
const fileDataString = fs.readFileSync(
|
|
149
|
+
path.resolve(consts.TEXT_DIR, directoryContents[0]),
|
|
150
|
+
"utf8"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
switch (format) {
|
|
154
|
+
case "android":
|
|
155
|
+
case "ios-strings":
|
|
156
|
+
case "ios-stringsdict":
|
|
157
|
+
expect(typeof data).toBe("string");
|
|
158
|
+
expect(fileDataString.replace(/\s/g, "")).toEqual(
|
|
159
|
+
(data as string).replace(/\s/g, "")
|
|
160
|
+
);
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case "flat":
|
|
164
|
+
case "nested":
|
|
165
|
+
case "structured":
|
|
166
|
+
case "icu":
|
|
167
|
+
default:
|
|
168
|
+
expect(JSON.parse(fileDataString)).toEqual(data);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
formats.forEach(({ format, data, ext }) => {
|
|
174
|
+
it(`writes the ${format} format to disk`, async () => {
|
|
175
|
+
mockApiCall(data);
|
|
176
|
+
await verifySavedData(format, data, ext);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("getFormatDataIsValid", () => {
|
|
182
|
+
it("handles flat format appropriately", () => {
|
|
183
|
+
expect(getFormatDataIsValid.flat("{}")).toBe(false);
|
|
184
|
+
expect(getFormatDataIsValid.flat(`{ "hello": "world" }`)).toBe(true);
|
|
185
|
+
expect(
|
|
186
|
+
getFormatDataIsValid.flat(`{
|
|
187
|
+
"__variant-name": "English",
|
|
188
|
+
"__variant-description": ""
|
|
189
|
+
}`)
|
|
190
|
+
).toBe(false);
|
|
191
|
+
expect(
|
|
192
|
+
getFormatDataIsValid.flat(`{
|
|
193
|
+
"__variant-name": "English",
|
|
194
|
+
"__variant-description": "",
|
|
195
|
+
"hello": "world"
|
|
196
|
+
}`)
|
|
197
|
+
).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
it("handles structured format appropriately", () => {
|
|
200
|
+
expect(getFormatDataIsValid.structured("{}")).toBe(false);
|
|
201
|
+
expect(
|
|
202
|
+
getFormatDataIsValid.structured(`{ "hello": { "text": "world" } }`)
|
|
203
|
+
).toBe(true);
|
|
204
|
+
expect(
|
|
205
|
+
getFormatDataIsValid.structured(`{
|
|
206
|
+
"__variant-name": "English",
|
|
207
|
+
"__variant-description": ""
|
|
208
|
+
}`)
|
|
209
|
+
).toBe(false);
|
|
210
|
+
expect(
|
|
211
|
+
getFormatDataIsValid.structured(`{
|
|
212
|
+
"__variant-name": "English",
|
|
213
|
+
"__variant-description": "",
|
|
214
|
+
"hello": { "text": "world" }
|
|
215
|
+
}`)
|
|
216
|
+
).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
it("handles icu format appropriately", () => {
|
|
219
|
+
expect(getFormatDataIsValid.icu("{}")).toBe(false);
|
|
220
|
+
expect(getFormatDataIsValid.icu(`{ "hello": "world" }`)).toBe(true);
|
|
221
|
+
expect(
|
|
222
|
+
getFormatDataIsValid.icu(`{
|
|
223
|
+
"__variant-name": "English",
|
|
224
|
+
"__variant-description": ""
|
|
225
|
+
}`)
|
|
226
|
+
).toBe(false);
|
|
227
|
+
expect(
|
|
228
|
+
getFormatDataIsValid.icu(`{
|
|
229
|
+
"__variant-name": "English",
|
|
230
|
+
"__variant-description": "",
|
|
231
|
+
"hello": "world"
|
|
232
|
+
}`)
|
|
233
|
+
).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
it("handles android format appropriately", () => {
|
|
236
|
+
expect(
|
|
237
|
+
getFormatDataIsValid.android(`
|
|
238
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
239
|
+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>
|
|
240
|
+
`)
|
|
241
|
+
).toBe(false);
|
|
242
|
+
expect(
|
|
243
|
+
getFormatDataIsValid.android(`
|
|
244
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
245
|
+
<!--Variant Name: English-->
|
|
246
|
+
<!--Variant Description: -->
|
|
247
|
+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>
|
|
248
|
+
`)
|
|
249
|
+
).toBe(false);
|
|
250
|
+
expect(
|
|
251
|
+
getFormatDataIsValid.android(`
|
|
252
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
253
|
+
<!--Variant Name: English-->
|
|
254
|
+
<!--Variant Description: -->
|
|
255
|
+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
|
256
|
+
<string name="hello-world" ditto_api_id="hello-world">Hello World</string>
|
|
257
|
+
</resources>
|
|
258
|
+
`)
|
|
259
|
+
).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
it("handles ios-strings format appropriately", () => {
|
|
262
|
+
expect(getFormatDataIsValid["ios-strings"]("")).toBe(false);
|
|
263
|
+
expect(
|
|
264
|
+
getFormatDataIsValid["ios-strings"](`
|
|
265
|
+
/* Variant Name: English */
|
|
266
|
+
/* Variant Description: */
|
|
267
|
+
`)
|
|
268
|
+
).toBe(false);
|
|
269
|
+
expect(
|
|
270
|
+
getFormatDataIsValid["ios-strings"](`
|
|
271
|
+
/* Variant Name: English */
|
|
272
|
+
/* Variant Description: */
|
|
273
|
+
"Hello" = "World";
|
|
274
|
+
`)
|
|
275
|
+
).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
it("handles ios-stringsdict format appropriately", () => {
|
|
278
|
+
expect(
|
|
279
|
+
getFormatDataIsValid["ios-stringsdict"](`
|
|
280
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
281
|
+
<plist version="1.0">
|
|
282
|
+
<dict/>
|
|
283
|
+
</plist>
|
|
284
|
+
`)
|
|
285
|
+
).toBe(false);
|
|
286
|
+
expect(
|
|
287
|
+
getFormatDataIsValid["ios-stringsdict"](`
|
|
288
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
289
|
+
<!--Variant Name: English-->
|
|
290
|
+
<!--Variant Description: -->
|
|
291
|
+
<plist version="1.0">
|
|
292
|
+
<dict/>
|
|
293
|
+
</plist>
|
|
294
|
+
`)
|
|
295
|
+
).toBe(false);
|
|
296
|
+
expect(
|
|
297
|
+
getFormatDataIsValid["ios-stringsdict"](`
|
|
298
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
299
|
+
<!--Variant Name: English-->
|
|
300
|
+
<!--Variant Description: -->
|
|
301
|
+
<plist version="1.0">
|
|
302
|
+
<dict>
|
|
303
|
+
<key>Hello World</key>
|
|
304
|
+
<dict>
|
|
305
|
+
<key>NSStringLocalizedFormatKey</key>
|
|
306
|
+
<string>%1$#@count@</string>
|
|
307
|
+
<key>count</key>
|
|
308
|
+
<dict>
|
|
309
|
+
<key>NSStringFormatSpecTypeKey</key>
|
|
310
|
+
<string>NSStringPluralRuleType</string>
|
|
311
|
+
<key>NSStringFormatValueTypeKey</key>
|
|
312
|
+
<string>d</string>
|
|
313
|
+
<key>other</key>
|
|
314
|
+
<string>espanol</string>
|
|
315
|
+
</dict>
|
|
316
|
+
</dict>
|
|
317
|
+
</dict>
|
|
318
|
+
</plist>
|
|
319
|
+
`)
|
|
320
|
+
).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const { getJsonFormat } = _test;
|
|
325
|
+
describe("getJsonFormat", () => {
|
|
326
|
+
it("returns 'flat' if no format specified", () => {
|
|
327
|
+
expect(getJsonFormat([])).toBe("flat");
|
|
328
|
+
});
|
|
329
|
+
it("returns 'flat' if invalid format specified", () => {
|
|
330
|
+
expect(getJsonFormat(["invalid-format"])).toBe("flat");
|
|
331
|
+
});
|
|
332
|
+
it("returns valid specified formats", () => {
|
|
333
|
+
expect(getJsonFormat(["structured"])).toBe("structured");
|
|
334
|
+
expect(getJsonFormat(["nested"])).toBe("nested");
|
|
335
|
+
expect(getJsonFormat(["icu"])).toBe("icu");
|
|
336
|
+
expect(getJsonFormat(["flat"])).toBe("flat");
|
|
337
|
+
});
|
|
338
|
+
it("returns last of formats if multiple specified", () => {
|
|
339
|
+
expect(getJsonFormat(["flat", "structured", "icu"])).toBe("icu");
|
|
340
|
+
expect(getJsonFormat(["flat", "icu", "structured"])).toBe("structured");
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const { getJsonFormatIsValid } = _test;
|
|
345
|
+
describe("getJsonFormatIsValid", () => {
|
|
346
|
+
it("returns true for valid json", () => {
|
|
347
|
+
expect(getJsonFormatIsValid(`{ "key": "value" }`)).toBe(true);
|
|
348
|
+
expect(getJsonFormatIsValid(`{ "key": { "text": "value" }}`)).toBe(true);
|
|
349
|
+
expect(getJsonFormatIsValid(`{ "nested": { "key": "value" }}`)).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
it("returns false for empty json", () => {
|
|
352
|
+
expect(getJsonFormatIsValid(`{}`)).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
it("returns false for invalid json", () => {
|
|
355
|
+
expect(getJsonFormatIsValid(`abcdefg`)).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const { ensureEndsWithNewLine } = _test;
|
|
360
|
+
describe("ensureEndsWithNewLine", () => {
|
|
361
|
+
it("adds a newline to the end of a string if it doesn't already have one", () => {
|
|
362
|
+
expect(ensureEndsWithNewLine("hello")).toBe("hello\n");
|
|
363
|
+
});
|
|
364
|
+
it("doesn't add a newline to the end of a string if it already has one", () => {
|
|
365
|
+
expect(ensureEndsWithNewLine("hello\n")).toBe("hello\n");
|
|
366
|
+
});
|
|
367
|
+
});
|