@inlang/sdk 0.26.5 → 0.28.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/dist/adapter/solidAdapter.test.js +52 -4
- package/dist/api.d.ts +5 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/createMessageLintReportsQuery.d.ts +1 -1
- package/dist/createMessageLintReportsQuery.d.ts.map +1 -1
- package/dist/createMessageLintReportsQuery.js +30 -22
- package/dist/createMessagesQuery.d.ts.map +1 -1
- package/dist/createMessagesQuery.js +24 -1
- package/dist/createMessagesQuery.test.js +46 -2
- package/dist/createNodeishFsWithAbsolutePaths.d.ts +3 -3
- package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
- package/dist/createNodeishFsWithAbsolutePaths.js +9 -1
- package/dist/createNodeishFsWithAbsolutePaths.test.js +7 -1
- package/dist/createNodeishFsWithWatcher.d.ts +3 -3
- package/dist/createNodeishFsWithWatcher.d.ts.map +1 -1
- package/dist/createNodeishFsWithWatcher.js +3 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +12 -0
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +474 -39
- package/dist/loadProject.test.js +93 -14
- package/dist/messages/variant.test.js +3 -0
- package/dist/resolve-modules/plugins/resolvePlugins.test.js +4 -2
- package/dist/storage/helper.d.ts +5 -0
- package/dist/storage/helper.d.ts.map +1 -0
- package/dist/storage/helper.js +35 -0
- package/dist/storage/human-id/human-readable-id.d.ts +3 -0
- package/dist/storage/human-id/human-readable-id.d.ts.map +1 -0
- package/dist/storage/human-id/human-readable-id.js +20 -0
- package/dist/storage/human-id/words.d.ts +5 -0
- package/dist/storage/human-id/words.d.ts.map +1 -0
- package/dist/storage/human-id/words.js +1032 -0
- package/dist/storage/human-id/words.test.d.ts +2 -0
- package/dist/storage/human-id/words.test.d.ts.map +1 -0
- package/dist/storage/human-id/words.test.js +25 -0
- package/dist/test-utilities/createMessage.d.ts +1 -0
- package/dist/test-utilities/createMessage.d.ts.map +1 -1
- package/dist/test-utilities/createMessage.js +1 -0
- package/dist/test-utilities/createMessage.test.js +70 -55
- package/package.json +16 -7
- package/src/adapter/solidAdapter.test.ts +76 -4
- package/src/api.ts +6 -0
- package/src/createMessageLintReportsQuery.ts +47 -35
- package/src/createMessagesQuery.test.ts +54 -2
- package/src/createMessagesQuery.ts +30 -1
- package/src/createNodeishFsWithAbsolutePaths.test.ts +10 -3
- package/src/createNodeishFsWithAbsolutePaths.ts +14 -5
- package/src/createNodeishFsWithWatcher.ts +6 -3
- package/src/errors.ts +20 -0
- package/src/loadProject.test.ts +108 -14
- package/src/loadProject.ts +657 -60
- package/src/messages/variant.test.ts +3 -0
- package/src/resolve-modules/plugins/resolvePlugins.test.ts +4 -2
- package/src/storage/helper.ts +48 -0
- package/src/storage/human-id/human-readable-id.ts +27 -0
- package/src/storage/human-id/words.test.ts +27 -0
- package/src/storage/human-id/words.ts +1035 -0
- package/src/test-utilities/createMessage.test.ts +72 -54
- package/src/test-utilities/createMessage.ts +1 -0
package/dist/loadProject.test.js
CHANGED
|
@@ -45,6 +45,7 @@ const mockPlugin = {
|
|
|
45
45
|
const exampleMessages = [
|
|
46
46
|
{
|
|
47
47
|
id: "a",
|
|
48
|
+
alias: {},
|
|
48
49
|
selectors: [],
|
|
49
50
|
variants: [
|
|
50
51
|
{
|
|
@@ -61,6 +62,47 @@ const exampleMessages = [
|
|
|
61
62
|
},
|
|
62
63
|
{
|
|
63
64
|
id: "b",
|
|
65
|
+
alias: {},
|
|
66
|
+
selectors: [],
|
|
67
|
+
variants: [
|
|
68
|
+
{
|
|
69
|
+
languageTag: "en",
|
|
70
|
+
match: [],
|
|
71
|
+
pattern: [
|
|
72
|
+
{
|
|
73
|
+
type: "Text",
|
|
74
|
+
value: "test",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const exampleAliasedMessages = [
|
|
82
|
+
{
|
|
83
|
+
id: "raw_tapir_pause_grateful",
|
|
84
|
+
alias: {
|
|
85
|
+
default: "a",
|
|
86
|
+
},
|
|
87
|
+
selectors: [],
|
|
88
|
+
variants: [
|
|
89
|
+
{
|
|
90
|
+
languageTag: "en",
|
|
91
|
+
match: [],
|
|
92
|
+
pattern: [
|
|
93
|
+
{
|
|
94
|
+
type: "Text",
|
|
95
|
+
value: "test",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "dizzy_halibut_dial_vaguely",
|
|
103
|
+
alias: {
|
|
104
|
+
default: "b",
|
|
105
|
+
},
|
|
64
106
|
selectors: [],
|
|
65
107
|
variants: [
|
|
66
108
|
{
|
|
@@ -405,6 +447,7 @@ describe("functionality", () => {
|
|
|
405
447
|
description: mockPlugin.description,
|
|
406
448
|
displayName: mockPlugin.displayName,
|
|
407
449
|
module: settings.modules[0],
|
|
450
|
+
settingsSchema: mockPlugin.settingsSchema,
|
|
408
451
|
});
|
|
409
452
|
expect(project.installed.messageLintRules()[0]).toEqual({
|
|
410
453
|
id: mockMessageLintRule.id,
|
|
@@ -412,6 +455,7 @@ describe("functionality", () => {
|
|
|
412
455
|
displayName: mockMessageLintRule.displayName,
|
|
413
456
|
module: settings.modules[1],
|
|
414
457
|
level: "warning",
|
|
458
|
+
settingsSchema: mockMessageLintRule.settingsSchema,
|
|
415
459
|
});
|
|
416
460
|
});
|
|
417
461
|
it("should apply 'warning' as default lint level to lint rules that have no lint level defined in the settings", async () => {
|
|
@@ -449,7 +493,7 @@ describe("functionality", () => {
|
|
|
449
493
|
id: "plugin.project.i18next",
|
|
450
494
|
description: { en: "Mock plugin description" },
|
|
451
495
|
displayName: { en: "Mock Plugin" },
|
|
452
|
-
loadMessages: () => [{ id: "some-message", selectors: [], variants: [] }],
|
|
496
|
+
loadMessages: () => [{ id: "some-message", alias: {}, selectors: [], variants: [] }],
|
|
453
497
|
saveMessages: () => undefined,
|
|
454
498
|
};
|
|
455
499
|
const repo = await mockRepo();
|
|
@@ -492,7 +536,7 @@ describe("functionality", () => {
|
|
|
492
536
|
id: "plugin.project.i18next",
|
|
493
537
|
description: { en: "Mock plugin description" },
|
|
494
538
|
displayName: { en: "Mock Plugin" },
|
|
495
|
-
loadMessages: () => [{ id: "some-message", selectors: [], variants: [] }],
|
|
539
|
+
loadMessages: () => [{ id: "some-message", alias: {}, selectors: [], variants: [] }],
|
|
496
540
|
saveMessages: () => undefined,
|
|
497
541
|
};
|
|
498
542
|
const repo = await mockRepo();
|
|
@@ -563,6 +607,20 @@ describe("functionality", () => {
|
|
|
563
607
|
expect(Object.values(project.query.messages.getAll())).toEqual(exampleMessages);
|
|
564
608
|
});
|
|
565
609
|
});
|
|
610
|
+
describe("messages with aliases", () => {
|
|
611
|
+
it("should return the messages", async () => {
|
|
612
|
+
const repo = await mockRepo();
|
|
613
|
+
const fs = repo.nodeishFs;
|
|
614
|
+
await fs.mkdir("/user/project.inlang", { recursive: true });
|
|
615
|
+
await fs.writeFile("/user/project.inlang/settings.json", JSON.stringify({ ...settings, experimental: { aliases: true } }));
|
|
616
|
+
const project = await loadProject({
|
|
617
|
+
projectPath: "/user/project.inlang",
|
|
618
|
+
repo,
|
|
619
|
+
_import,
|
|
620
|
+
});
|
|
621
|
+
expect(Object.values(project.query.messages.getAll())).toEqual(exampleAliasedMessages);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
566
624
|
describe("query", () => {
|
|
567
625
|
it("should call saveMessages() on updates", async () => {
|
|
568
626
|
const repo = await mockRepo();
|
|
@@ -600,6 +658,7 @@ describe("functionality", () => {
|
|
|
600
658
|
where: { id: "a" },
|
|
601
659
|
data: {
|
|
602
660
|
id: "a",
|
|
661
|
+
alias: {},
|
|
603
662
|
selectors: [],
|
|
604
663
|
variants: [
|
|
605
664
|
{
|
|
@@ -629,6 +688,7 @@ describe("functionality", () => {
|
|
|
629
688
|
where: { id: "b" },
|
|
630
689
|
data: {
|
|
631
690
|
id: "b",
|
|
691
|
+
alias: {},
|
|
632
692
|
selectors: [],
|
|
633
693
|
variants: [
|
|
634
694
|
{
|
|
@@ -654,31 +714,33 @@ describe("functionality", () => {
|
|
|
654
714
|
],
|
|
655
715
|
},
|
|
656
716
|
});
|
|
657
|
-
|
|
717
|
+
// lets wait for the next tick
|
|
718
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
658
719
|
expect(mockSaveFn.mock.calls.length).toBe(1);
|
|
659
720
|
expect(mockSaveFn.mock.calls[0][0].settings).toStrictEqual(settings);
|
|
660
721
|
expect(Object.values(mockSaveFn.mock.calls[0][0].messages)).toStrictEqual([
|
|
661
722
|
{
|
|
662
723
|
id: "a",
|
|
724
|
+
alias: {},
|
|
663
725
|
selectors: [],
|
|
664
726
|
variants: [
|
|
665
727
|
{
|
|
666
|
-
languageTag: "
|
|
728
|
+
languageTag: "de",
|
|
667
729
|
match: [],
|
|
668
730
|
pattern: [
|
|
669
731
|
{
|
|
670
732
|
type: "Text",
|
|
671
|
-
value: "a
|
|
733
|
+
value: "a de",
|
|
672
734
|
},
|
|
673
735
|
],
|
|
674
736
|
},
|
|
675
737
|
{
|
|
676
|
-
languageTag: "
|
|
738
|
+
languageTag: "en",
|
|
677
739
|
match: [],
|
|
678
740
|
pattern: [
|
|
679
741
|
{
|
|
680
742
|
type: "Text",
|
|
681
|
-
value: "a
|
|
743
|
+
value: "a en",
|
|
682
744
|
},
|
|
683
745
|
],
|
|
684
746
|
},
|
|
@@ -686,25 +748,26 @@ describe("functionality", () => {
|
|
|
686
748
|
},
|
|
687
749
|
{
|
|
688
750
|
id: "b",
|
|
751
|
+
alias: {},
|
|
689
752
|
selectors: [],
|
|
690
753
|
variants: [
|
|
691
754
|
{
|
|
692
|
-
languageTag: "
|
|
755
|
+
languageTag: "de",
|
|
693
756
|
match: [],
|
|
694
757
|
pattern: [
|
|
695
758
|
{
|
|
696
759
|
type: "Text",
|
|
697
|
-
value: "b
|
|
760
|
+
value: "b de",
|
|
698
761
|
},
|
|
699
762
|
],
|
|
700
763
|
},
|
|
701
764
|
{
|
|
702
|
-
languageTag: "
|
|
765
|
+
languageTag: "en",
|
|
703
766
|
match: [],
|
|
704
767
|
pattern: [
|
|
705
768
|
{
|
|
706
769
|
type: "Text",
|
|
707
|
-
value: "b
|
|
770
|
+
value: "b en",
|
|
708
771
|
},
|
|
709
772
|
],
|
|
710
773
|
},
|
|
@@ -766,6 +829,14 @@ describe("functionality", () => {
|
|
|
766
829
|
await new Promise((resolve) => setTimeout(resolve, 510));
|
|
767
830
|
expect(mockSaveFn.mock.calls.length).toBe(1);
|
|
768
831
|
expect(mockSaveFn.mock.calls[0][0].messages).toHaveLength(4);
|
|
832
|
+
project.query.messages.create({ data: createMessage("fifth", { en: "fifth message" }) });
|
|
833
|
+
await new Promise((resolve) => setTimeout(resolve, 510));
|
|
834
|
+
expect(mockSaveFn.mock.calls.length).toBe(2);
|
|
835
|
+
expect(mockSaveFn.mock.calls[1][0].messages).toHaveLength(5);
|
|
836
|
+
project.query.messages.delete({ where: { id: "fourth" } });
|
|
837
|
+
await new Promise((resolve) => setTimeout(resolve, 510));
|
|
838
|
+
expect(mockSaveFn.mock.calls.length).toBe(3);
|
|
839
|
+
expect(mockSaveFn.mock.calls[2][0].messages).toHaveLength(4);
|
|
769
840
|
});
|
|
770
841
|
});
|
|
771
842
|
describe("lint", () => {
|
|
@@ -868,14 +939,22 @@ describe("functionality", () => {
|
|
|
868
939
|
project.query.messages.getAll.subscribe(() => {
|
|
869
940
|
counter = counter + 1;
|
|
870
941
|
});
|
|
942
|
+
// subscribe fires once
|
|
871
943
|
expect(counter).toBe(1);
|
|
872
|
-
//
|
|
944
|
+
// saving the file without changing should not trigger a message query
|
|
873
945
|
await fs.writeFile("./messages.json", JSON.stringify(messages));
|
|
874
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
946
|
+
await new Promise((resolve) => setTimeout(resolve, 200)); // file event will lock a file and be handled sequentially - give it time to pickup the change
|
|
947
|
+
// we didn't change the message we write into message.json - shouldn't change the messages
|
|
948
|
+
expect(counter).toBe(1);
|
|
949
|
+
// saving the file without changing should trigger a change
|
|
950
|
+
messages.data[0].variants[0].pattern[0].value = "changed";
|
|
951
|
+
await fs.writeFile("./messages.json", JSON.stringify(messages));
|
|
952
|
+
await new Promise((resolve) => setTimeout(resolve, 200)); // file event will lock a file and be handled sequentially - give it time to pickup the change
|
|
875
953
|
expect(counter).toBe(2);
|
|
954
|
+
messages.data[0].variants[0].pattern[0].value = "changed3";
|
|
876
955
|
// change file
|
|
877
956
|
await fs.writeFile("./messages.json", JSON.stringify(messages));
|
|
878
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
957
|
+
await new Promise((resolve) => setTimeout(resolve, 200)); // file event will lock a file and be handled sequentially - give it time to pickup the change
|
|
879
958
|
expect(counter).toBe(3);
|
|
880
959
|
});
|
|
881
960
|
});
|
|
@@ -32,6 +32,7 @@ describe("getVariant", () => {
|
|
|
32
32
|
test("it should not throw error if selector is empty and match", () => {
|
|
33
33
|
const mockMessage = {
|
|
34
34
|
id: "mockMessage",
|
|
35
|
+
alias: {},
|
|
35
36
|
selectors: [],
|
|
36
37
|
variants: [
|
|
37
38
|
{
|
|
@@ -57,6 +58,7 @@ describe("getVariant", () => {
|
|
|
57
58
|
test("it should not throw error if selector is empty, return undefined", () => {
|
|
58
59
|
const mockMessage = {
|
|
59
60
|
id: "mockMessage",
|
|
61
|
+
alias: {},
|
|
60
62
|
selectors: [],
|
|
61
63
|
variants: [
|
|
62
64
|
{
|
|
@@ -326,6 +328,7 @@ describe("updateVariant", () => {
|
|
|
326
328
|
const getMockMessage = () => {
|
|
327
329
|
return {
|
|
328
330
|
id: "first-message",
|
|
331
|
+
alias: {},
|
|
329
332
|
selectors: [
|
|
330
333
|
{ type: "VariableReference", name: "gender" },
|
|
331
334
|
{ type: "VariableReference", name: "guestOther" },
|
|
@@ -57,7 +57,9 @@ describe("loadMessages", () => {
|
|
|
57
57
|
id: "plugin.namespace.placeholder",
|
|
58
58
|
description: { en: "My plugin description" },
|
|
59
59
|
displayName: { en: "My plugin" },
|
|
60
|
-
loadMessages: async () => [
|
|
60
|
+
loadMessages: async () => [
|
|
61
|
+
{ id: "test", alias: {}, expressions: [], selectors: [], variants: [] },
|
|
62
|
+
],
|
|
61
63
|
};
|
|
62
64
|
const resolved = await resolvePlugins({
|
|
63
65
|
plugins: [mockPlugin],
|
|
@@ -67,7 +69,7 @@ describe("loadMessages", () => {
|
|
|
67
69
|
expect(await resolved.data.loadMessages({
|
|
68
70
|
settings: {},
|
|
69
71
|
nodeishFs: {},
|
|
70
|
-
})).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }]);
|
|
72
|
+
})).toEqual([{ id: "test", alias: {}, expressions: [], selectors: [], variants: [] }]);
|
|
71
73
|
});
|
|
72
74
|
it("should collect an error if function is defined twice in multiple plugins", async () => {
|
|
73
75
|
const mockPlugin = {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Message } from "../versionedInterfaces.js";
|
|
2
|
+
export declare function getMessageIdFromPath(path: string): string | undefined;
|
|
3
|
+
export declare function getPathFromMessageId(id: string): string;
|
|
4
|
+
export declare function stringifyMessage(message: Message): string;
|
|
5
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../src/storage/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAW,MAAM,2BAA2B,CAAA;AAI5D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,sBAahD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,UAG9C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,UAuBhD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Message, Variant } from "../versionedInterfaces.js";
|
|
2
|
+
const fileExtension = ".json";
|
|
3
|
+
export function getMessageIdFromPath(path) {
|
|
4
|
+
if (!path.endsWith(fileExtension)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const cleanedPath = path.replace(/\/$/, ""); // This regex matches a trailing slash and replaces it with an empty string
|
|
8
|
+
const messageFileName = cleanedPath.split("/").join("_"); // we split by the first leading namespace or _ separator - make sure slashes don't exit in the id
|
|
9
|
+
// const messageFileName = pathParts.at(-1)!
|
|
10
|
+
const lastDotIndex = messageFileName.lastIndexOf(".");
|
|
11
|
+
// Extract until the last dot (excluding the dot)
|
|
12
|
+
return messageFileName.slice(0, Math.max(0, lastDotIndex));
|
|
13
|
+
}
|
|
14
|
+
export function getPathFromMessageId(id) {
|
|
15
|
+
const path = id.replace("_", "/") + fileExtension;
|
|
16
|
+
return path;
|
|
17
|
+
}
|
|
18
|
+
export function stringifyMessage(message) {
|
|
19
|
+
// create a new object do specify key output order
|
|
20
|
+
const messageWithSortedKeys = {};
|
|
21
|
+
for (const key of Object.keys(message).sort()) {
|
|
22
|
+
messageWithSortedKeys[key] = message[key];
|
|
23
|
+
}
|
|
24
|
+
// lets order variants as well
|
|
25
|
+
messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"].sort((variantA, variantB) => {
|
|
26
|
+
// First, compare by language
|
|
27
|
+
const languageComparison = variantA.languageTag.localeCompare(variantB.languageTag);
|
|
28
|
+
// If languages are the same, compare by match
|
|
29
|
+
if (languageComparison === 0) {
|
|
30
|
+
return variantA.match.join("-").localeCompare(variantB.match.join("-"));
|
|
31
|
+
}
|
|
32
|
+
return languageComparison;
|
|
33
|
+
});
|
|
34
|
+
return JSON.stringify(messageWithSortedKeys, undefined, 4);
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-readable-id.d.ts","sourceRoot":"","sources":["../../../src/storage/human-id/human-readable-id.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,WAI5B;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU,UAgB5D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// we use murmur for best distribution https://medium.com/miro-engineering/choosing-a-hash-function-to-solve-a-data-sharding-problem-c656259e2b54
|
|
2
|
+
import murmurhash3 from "murmurhash3js";
|
|
3
|
+
import { adjectives, animals, adverbs, verbs } from "./words.js";
|
|
4
|
+
export function randomHumanId() {
|
|
5
|
+
return `${adjectives[Math.floor(Math.random() * 256)]}_${adjectives[Math.floor(Math.random() * 256)]}_${animals[Math.floor(Math.random() * 256)]}_${verbs[Math.floor(Math.random() * 256)]}`;
|
|
6
|
+
}
|
|
7
|
+
export function humanIdHash(value, offset = 0) {
|
|
8
|
+
// Seed value can be any arbitrary value
|
|
9
|
+
const seed = 42;
|
|
10
|
+
// Use MurmurHash3 to produce a 32-bit hash
|
|
11
|
+
const hash32 = murmurhash3.x86.hash32(value, seed);
|
|
12
|
+
// Add 1 and take modulo 2^32 to fit within 32 bits
|
|
13
|
+
const hash32WithOffset = (hash32 + offset) >>> 0;
|
|
14
|
+
// Extract four 8-bit parts
|
|
15
|
+
const part1 = (hash32WithOffset >>> 24) & 0xff;
|
|
16
|
+
const part2 = (hash32WithOffset >>> 16) & 0xff;
|
|
17
|
+
const part3 = (hash32WithOffset >>> 8) & 0xff;
|
|
18
|
+
const part4 = hash32WithOffset & 0xff;
|
|
19
|
+
return `${adjectives[part1]}_${animals[part2]}_${verbs[part3]}_${adverbs[part4]}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../../../src/storage/human-id/words.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAiQnB,CAAA;AAED,eAAO,MAAM,UAAU,UAiQtB,CAAA;AAED,eAAO,MAAM,OAAO,UAiQnB,CAAA;AAED,eAAO,MAAM,KAAK,UAiQjB,CAAA"}
|