@platformos/platformos-common 0.0.2

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Utility functions for identifying platformOS file types based on their paths
3
+ */
4
+
5
+ import { UriString } from './AbstractFileSystem';
6
+
7
+ /**
8
+ * Checks if a URI points to a partial file.
9
+ * Partials can be located in:
10
+ * - app/lib
11
+ * - app/views/partials
12
+ * - app/modules/{moduleName}/public/lib
13
+ * - app/modules/{moduleName}/private/lib
14
+ * - app/modules/{moduleName}/public/views/partials
15
+ * - app/modules/{moduleName}/private/views/partials
16
+ * - modules/{moduleName}/public/lib
17
+ * - modules/{moduleName}/private/lib
18
+ * - modules/{moduleName}/public/views/partials
19
+ * - modules/{moduleName}/private/views/partials
20
+ */
21
+ export function isPartial(uri: UriString): boolean {
22
+ return uri.includes('/lib/') || uri.includes('/views/partials');
23
+ }
24
+
25
+ /**
26
+ * Checks if a URI points to a page file.
27
+ * Pages are located in app/views/pages
28
+ */
29
+ export function isPage(uri: UriString): boolean {
30
+ return uri.includes('/views/pages');
31
+ }
32
+
33
+ /**
34
+ * Checks if a URI points to a layout file.
35
+ * Layouts are located in app/views/layouts
36
+ */
37
+ export function isLayout(uri: UriString): boolean {
38
+ return uri.includes('/views/layouts');
39
+ }
40
+
41
+ /**
42
+ * Legacy Shopify terminology - use isPartial instead
43
+ * @deprecated Use isPartial instead
44
+ */
45
+ export const isSnippet = isPartial;
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { URI } from 'vscode-uri';
3
+ import { TranslationProvider } from './TranslationProvider';
4
+ import { AbstractFileSystem, FileType, FileStat, FileTuple } from '../AbstractFileSystem';
5
+
6
+ function createMockFileSystem(files: Record<string, string>): AbstractFileSystem {
7
+ const fileSet = new Set(Object.keys(files));
8
+
9
+ return {
10
+ stat: vi.fn(async (uri: string): Promise<FileStat> => {
11
+ if (fileSet.has(uri)) {
12
+ return { type: FileType.File, size: files[uri].length };
13
+ }
14
+ throw new Error(`File not found: ${uri}`);
15
+ }),
16
+ readFile: vi.fn(async (uri: string): Promise<string> => {
17
+ if (fileSet.has(uri)) {
18
+ return files[uri];
19
+ }
20
+ throw new Error(`File not found: ${uri}`);
21
+ }),
22
+ readDirectory: vi.fn(async (_uri: string): Promise<FileTuple[]> => []),
23
+ };
24
+ }
25
+
26
+ describe('TranslationProvider', () => {
27
+ const rootUri = URI.parse('file:///project');
28
+
29
+ describe('findTranslationFile', () => {
30
+ it('should find translation file in app/translations', async () => {
31
+ const fs = createMockFileSystem({
32
+ 'file:///project/app/translations/en/general.yml': 'en:\n hello: Hello',
33
+ });
34
+ const provider = new TranslationProvider(fs);
35
+
36
+ const [file, key] = await provider.findTranslationFile(rootUri, 'general.hello', 'en');
37
+
38
+ expect(file).toBe('file:///project/app/translations/en/general.yml');
39
+ expect(key).toBe('general.hello');
40
+ });
41
+
42
+ it('should find module translation file', async () => {
43
+ const fs = createMockFileSystem({
44
+ 'file:///project/app/modules/user/public/translations/en/messages.yml':
45
+ 'en:\n welcome: Welcome',
46
+ });
47
+ const provider = new TranslationProvider(fs);
48
+
49
+ const [file, key] = await provider.findTranslationFile(
50
+ rootUri,
51
+ 'modules/user/messages.welcome',
52
+ 'en',
53
+ );
54
+
55
+ expect(file).toBe('file:///project/app/modules/user/public/translations/en/messages.yml');
56
+ expect(key).toBe('messages.welcome');
57
+ });
58
+
59
+ it('should return undefined for non-existent translation file', async () => {
60
+ const fs = createMockFileSystem({});
61
+ const provider = new TranslationProvider(fs);
62
+
63
+ const [file, key] = await provider.findTranslationFile(rootUri, 'missing.key', 'en');
64
+
65
+ expect(file).toBeUndefined();
66
+ expect(key).toBeUndefined();
67
+ });
68
+
69
+ it('should return undefined for empty translation key', async () => {
70
+ const fs = createMockFileSystem({});
71
+ const provider = new TranslationProvider(fs);
72
+
73
+ const [file, key] = await provider.findTranslationFile(rootUri, '', 'en');
74
+
75
+ expect(file).toBeUndefined();
76
+ expect(key).toBeUndefined();
77
+ });
78
+ });
79
+
80
+ describe('translate', () => {
81
+ it('should translate a simple key', async () => {
82
+ const fs = createMockFileSystem({
83
+ 'file:///project/app/translations/en/general.yml':
84
+ 'en:\n general:\n hello: Hello World',
85
+ });
86
+ const provider = new TranslationProvider(fs);
87
+
88
+ const result = await provider.translate(rootUri, 'general.hello', 'en');
89
+
90
+ expect(result).toBe('Hello World');
91
+ });
92
+
93
+ it('should translate nested keys', async () => {
94
+ const fs = createMockFileSystem({
95
+ 'file:///project/app/translations/en/forms.yml':
96
+ 'en:\n forms:\n errors:\n required: This field is required',
97
+ });
98
+ const provider = new TranslationProvider(fs);
99
+
100
+ const result = await provider.translate(rootUri, 'forms.errors.required', 'en');
101
+
102
+ expect(result).toBe('This field is required');
103
+ });
104
+
105
+ it('should return undefined for missing nested key', async () => {
106
+ const fs = createMockFileSystem({
107
+ 'file:///project/app/translations/en/general.yml': 'en:\n existing: value',
108
+ });
109
+ const provider = new TranslationProvider(fs);
110
+
111
+ const result = await provider.translate(rootUri, 'general.missing.nested', 'en');
112
+
113
+ expect(result).toBeUndefined();
114
+ });
115
+
116
+ it('should translate module keys', async () => {
117
+ const fs = createMockFileSystem({
118
+ 'file:///project/app/modules/admin/public/translations/en/dashboard.yml':
119
+ 'en:\n dashboard:\n title: Admin Dashboard',
120
+ });
121
+ const provider = new TranslationProvider(fs);
122
+
123
+ const result = await provider.translate(rootUri, 'modules/admin/dashboard.title', 'en');
124
+
125
+ expect(result).toBe('Admin Dashboard');
126
+ });
127
+
128
+ it('should use default locale when not specified', async () => {
129
+ const fs = createMockFileSystem({
130
+ 'file:///project/app/translations/en/common.yml': 'en:\n common:\n yes: Yes',
131
+ });
132
+ const provider = new TranslationProvider(fs);
133
+
134
+ const result = await provider.translate(rootUri, 'common.yes');
135
+
136
+ expect(result).toBe('Yes');
137
+ });
138
+
139
+ it('should check private module translations when public does not exist', async () => {
140
+ const fs = createMockFileSystem({
141
+ 'file:///project/app/modules/internal/private/translations/en/secret.yml':
142
+ 'en:\n secret:\n message: Secret Message',
143
+ });
144
+ const provider = new TranslationProvider(fs);
145
+
146
+ const result = await provider.translate(rootUri, 'modules/internal/secret.message', 'en');
147
+
148
+ expect(result).toBe('Secret Message');
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,99 @@
1
+ import { AbstractFileSystem, FileType } from '../AbstractFileSystem';
2
+ import { URI, Utils } from 'vscode-uri';
3
+ import yaml from 'js-yaml';
4
+
5
+ type ModuleKeyInfo =
6
+ | { isModule: false; key: string }
7
+ | { isModule: true; moduleName: string; key: string };
8
+
9
+ export class TranslationProvider {
10
+ constructor(private readonly fs: AbstractFileSystem) {}
11
+
12
+ private async isFile(path: string): Promise<boolean> {
13
+ try {
14
+ return (await this.fs.stat(path)).type === FileType.File;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ private async readFileIfExists(path: string): Promise<string | undefined> {
21
+ return (await this.isFile(path)) ? this.fs.readFile(path) : undefined;
22
+ }
23
+
24
+ private parseModuleKey(translationKey: string): ModuleKeyInfo {
25
+ if (!translationKey.startsWith('modules/')) {
26
+ return { isModule: false, key: translationKey };
27
+ }
28
+
29
+ const [, moduleName, key] = translationKey.split('/', 3);
30
+
31
+ return key ? { isModule: true, moduleName, key } : { isModule: false, key: translationKey };
32
+ }
33
+
34
+ private getSearchPaths(moduleName?: string): string[] {
35
+ if (!moduleName) {
36
+ return ['app/translations'];
37
+ }
38
+
39
+ return [
40
+ `app/modules/${moduleName}/public/translations`,
41
+ `app/modules/${moduleName}/private/translations`,
42
+ `modules/${moduleName}/public/translations`,
43
+ `modules/${moduleName}/private/translations`,
44
+ ];
45
+ }
46
+
47
+ async findTranslationFile(
48
+ rootUri: URI,
49
+ translationKey: string,
50
+ defaultLocale: string,
51
+ ): Promise<[string | undefined, string | undefined]> {
52
+ const parsed = this.parseModuleKey(translationKey);
53
+ const fileName = parsed.key.split('.')[0];
54
+
55
+ if (!fileName) {
56
+ return [undefined, undefined];
57
+ }
58
+
59
+ const searchPaths = this.getSearchPaths(parsed.isModule ? parsed.moduleName : undefined);
60
+
61
+ for (const basePath of searchPaths) {
62
+ const uri = Utils.joinPath(rootUri, basePath, defaultLocale, `${fileName}.yml`).toString();
63
+
64
+ if (await this.isFile(uri)) {
65
+ return [uri, parsed.key];
66
+ }
67
+ }
68
+
69
+ return [undefined, undefined];
70
+ }
71
+
72
+ async translate(
73
+ rootUri: URI,
74
+ translationKey: string,
75
+ defaultLocale: string = 'en',
76
+ ): Promise<string | undefined> {
77
+ const [file, key] = await this.findTranslationFile(rootUri, translationKey, defaultLocale);
78
+
79
+ if (!file || !key) {
80
+ return undefined;
81
+ }
82
+
83
+ const contents = await this.readFileIfExists(file);
84
+ if (!contents) {
85
+ return undefined;
86
+ }
87
+
88
+ let data: any = yaml.load(contents);
89
+
90
+ for (const part of [defaultLocale, ...key.split('.')]) {
91
+ data = data?.[part];
92
+ if (data === undefined) {
93
+ return undefined;
94
+ }
95
+ }
96
+
97
+ return data;
98
+ }
99
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["**/*.spec.ts"],
4
+ "references": []
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": [
4
+ "./src/**/*.ts",
5
+ "./src/**/*.json"
6
+ ],
7
+ "exclude": [
8
+ "./dist"
9
+ ],
10
+ "compilerOptions": {
11
+ "outDir": "dist",
12
+ "rootDir": "src",
13
+ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
14
+ "paths": {
15
+ }
16
+ },
17
+ "references": [
18
+ ]
19
+ }