@ember-intl/utils 0.0.0-stage → 0.2.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/LICENSE.md +9 -0
- package/README.md +17 -2
- package/dist/config/find-user-config.d.ts +1 -0
- package/dist/config/find-user-config.js +13 -0
- package/dist/config/get-default-config.d.ts +2 -0
- package/dist/config/get-default-config.js +16 -0
- package/dist/config/get-user-config.d.ts +2 -0
- package/dist/config/get-user-config.js +45 -0
- package/dist/config/merge-configs.d.ts +2 -0
- package/dist/config/merge-configs.js +21 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.js +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +4 -0
- package/dist/lint-rules.d.ts +2 -0
- package/dist/lint-rules.js +5 -0
- package/dist/translations/find-translation-files.d.ts +3 -0
- package/dist/translations/find-translation-files.js +38 -0
- package/dist/translations/merge-translation-files/extract-translations.d.ts +8 -0
- package/dist/translations/merge-translation-files/extract-translations.js +54 -0
- package/dist/translations/merge-translation-files/index.d.ts +2 -0
- package/dist/translations/merge-translation-files/index.js +2 -0
- package/dist/translations/merge-translation-files/sort-translations.d.ts +2 -0
- package/dist/translations/merge-translation-files/sort-translations.js +8 -0
- package/dist/translations/merge-translation-files.d.ts +3 -0
- package/dist/translations/merge-translation-files.js +40 -0
- package/dist/translations/types.d.ts +18 -0
- package/dist/translations/types.js +1 -0
- package/dist/translations.d.ts +3 -0
- package/dist/translations.js +2 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +1 -0
- package/package.json +56 -3
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ember-intl/utils
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
_Utilities for `ember-intl` packages_
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Compatibility
|
|
7
|
+
|
|
8
|
+
- Node.js v22 or above
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
13
|
+
See the [Contributing](../../CONTRIBUTING.md) guide for details.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## License
|
|
17
|
+
|
|
18
|
+
This project is licensed under the [MIT License](./LICENSE.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findUserConfig(projectRoot: string): string | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { findFiles } from '@codemod-utils/files';
|
|
2
|
+
export function findUserConfig(projectRoot) {
|
|
3
|
+
const filePaths = findFiles('ember-intl.config.{js,mjs,mts,ts}', {
|
|
4
|
+
projectRoot,
|
|
5
|
+
});
|
|
6
|
+
if (filePaths.length === 0) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (filePaths.length > 1) {
|
|
10
|
+
throw new Error('Found multiple config files');
|
|
11
|
+
}
|
|
12
|
+
return filePaths[0];
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { lintRules } from '../lint-rules.js';
|
|
2
|
+
export function getDefaultConfig() {
|
|
3
|
+
const rules = {};
|
|
4
|
+
lintRules.forEach((lintRule) => {
|
|
5
|
+
rules[lintRule] = true;
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
addonPaths: [],
|
|
9
|
+
buildOptions: {
|
|
10
|
+
fallbackLocale: undefined,
|
|
11
|
+
namespaceKeysByDir: false,
|
|
12
|
+
translationsDir: 'translations',
|
|
13
|
+
},
|
|
14
|
+
lintRules: rules,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { lintRules } from '../lint-rules.js';
|
|
4
|
+
import { findUserConfig } from './find-user-config.js';
|
|
5
|
+
import { getDefaultConfig } from './get-default-config.js';
|
|
6
|
+
function validate(userConfig) {
|
|
7
|
+
if (userConfig === undefined) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const defaultConfig = getDefaultConfig();
|
|
11
|
+
if (userConfig.buildOptions) {
|
|
12
|
+
const buildOptions = new Set(Object.keys(defaultConfig.buildOptions));
|
|
13
|
+
Object.keys(userConfig.buildOptions).forEach((buildOption) => {
|
|
14
|
+
if (!buildOptions.has(buildOption)) {
|
|
15
|
+
throw new Error(`Unknown build option: ${buildOption}`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (userConfig.lintRules) {
|
|
20
|
+
Object.keys(userConfig.lintRules).forEach((lintRule) => {
|
|
21
|
+
if (!lintRules.includes(lintRule)) {
|
|
22
|
+
throw new Error(`Unknown lint rule: ${lintRule}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function getUserConfig(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const configFilePath = findUserConfig(projectRoot);
|
|
30
|
+
if (configFilePath === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const fileURL = pathToFileURL(join(projectRoot, configFilePath));
|
|
34
|
+
// Add timestamp to prevent caching the user config across tests
|
|
35
|
+
const filePath = `${fileURL.href}?update=${Date.now()}`;
|
|
36
|
+
const { default: userConfig } = (await import(filePath));
|
|
37
|
+
validate(userConfig);
|
|
38
|
+
return userConfig;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`ERROR: Unable to read the config file. (${error.message})`, {
|
|
42
|
+
cause: error,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function mergeConfigs(config, userConfig) {
|
|
2
|
+
if (userConfig === undefined) {
|
|
3
|
+
return config;
|
|
4
|
+
}
|
|
5
|
+
if (userConfig.addonPaths) {
|
|
6
|
+
config.addonPaths = Array.from(new Set([...config.addonPaths, ...userConfig.addonPaths]));
|
|
7
|
+
}
|
|
8
|
+
if (userConfig.buildOptions) {
|
|
9
|
+
config.buildOptions = {
|
|
10
|
+
...config.buildOptions,
|
|
11
|
+
...userConfig.buildOptions,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (userConfig.lintRules) {
|
|
15
|
+
config.lintRules = {
|
|
16
|
+
...config.lintRules,
|
|
17
|
+
...userConfig.lintRules,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LintRule } from '../lint-rules.js';
|
|
2
|
+
type LintRuleOptions = Record<string, unknown>;
|
|
3
|
+
export type Config = {
|
|
4
|
+
addonPaths: string[];
|
|
5
|
+
buildOptions: {
|
|
6
|
+
fallbackLocale: string | undefined;
|
|
7
|
+
namespaceKeysByDir: boolean;
|
|
8
|
+
translationsDir: string;
|
|
9
|
+
};
|
|
10
|
+
lintRules: Record<LintRule, boolean | LintRuleOptions>;
|
|
11
|
+
};
|
|
12
|
+
export type UserConfig = Partial<{
|
|
13
|
+
addonPaths: Config['addonPaths'];
|
|
14
|
+
buildOptions: Partial<Config['buildOptions']>;
|
|
15
|
+
lintRules: Partial<Config['lintRules']>;
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findUserConfig } from './config/find-user-config.js';
|
|
2
|
+
export { getDefaultConfig } from './config/get-default-config.js';
|
|
3
|
+
export { getUserConfig } from './config/get-user-config.js';
|
|
4
|
+
export { mergeConfigs } from './config/merge-configs.js';
|
|
5
|
+
export type { Config, UserConfig } from './config/types.js';
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join, normalize, sep } from 'node:path';
|
|
2
|
+
import { findFiles, parseFilePath } from '@codemod-utils/files';
|
|
3
|
+
function getPattern(translationsDir) {
|
|
4
|
+
return join(translationsDir, '**/*.{json,yaml,yml}').replaceAll(sep, '/');
|
|
5
|
+
}
|
|
6
|
+
export function findTranslationFiles(options) {
|
|
7
|
+
const { config, projectRoot } = options;
|
|
8
|
+
const { addonPaths, buildOptions } = config;
|
|
9
|
+
const translationFiles = new Map();
|
|
10
|
+
addonPaths.forEach((addonPath) => {
|
|
11
|
+
const translationsDir = join(addonPath, 'translations');
|
|
12
|
+
const filePaths = findFiles(getPattern(translationsDir), {
|
|
13
|
+
projectRoot,
|
|
14
|
+
});
|
|
15
|
+
filePaths.forEach((filePath) => {
|
|
16
|
+
const { name } = parseFilePath(filePath);
|
|
17
|
+
translationFiles.set(filePath, {
|
|
18
|
+
isInternal: false,
|
|
19
|
+
locale: name,
|
|
20
|
+
translationsDir,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
// App's translations take precedence
|
|
25
|
+
const translationsDir = normalize(buildOptions.translationsDir);
|
|
26
|
+
const filePaths = findFiles(getPattern(translationsDir), {
|
|
27
|
+
projectRoot,
|
|
28
|
+
});
|
|
29
|
+
filePaths.forEach((filePath) => {
|
|
30
|
+
const { name } = parseFilePath(filePath);
|
|
31
|
+
translationFiles.set(filePath, {
|
|
32
|
+
isInternal: true,
|
|
33
|
+
locale: name,
|
|
34
|
+
translationsDir,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return translationFiles;
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TranslationFilePath, TranslationJson } from '../types.js';
|
|
2
|
+
type Data = {
|
|
3
|
+
filePath: TranslationFilePath;
|
|
4
|
+
namespaceKeysByDir: boolean;
|
|
5
|
+
translationsDir: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function extractTranslations(file: string, data: Data): TranslationJson;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { relative, sep } from 'node:path';
|
|
2
|
+
import { parseFilePath } from '@codemod-utils/files';
|
|
3
|
+
import { load } from 'js-yaml';
|
|
4
|
+
function getPrefix(data) {
|
|
5
|
+
if (!data.namespaceKeysByDir) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
const { dir } = parseFilePath(data.filePath);
|
|
9
|
+
const relativePath = relative(data.translationsDir, dir);
|
|
10
|
+
if (relativePath === '') {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
const prefix = relativePath.replaceAll(sep, '.');
|
|
14
|
+
return `${prefix}.`;
|
|
15
|
+
}
|
|
16
|
+
function traverse(json, data) {
|
|
17
|
+
for (const key in json) {
|
|
18
|
+
if (!Object.hasOwn(json, key)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const value = json[key];
|
|
22
|
+
if (typeof value === 'object') {
|
|
23
|
+
traverse(value, {
|
|
24
|
+
callback: data.callback,
|
|
25
|
+
prefix: `${data.prefix}${key}.`,
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
data.callback(`${data.prefix}${key}`, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function extractTranslations(file, data) {
|
|
33
|
+
const translationJson = {};
|
|
34
|
+
if (file === '') {
|
|
35
|
+
return translationJson;
|
|
36
|
+
}
|
|
37
|
+
const { ext } = parseFilePath(data.filePath);
|
|
38
|
+
try {
|
|
39
|
+
const json = ext === '.json'
|
|
40
|
+
? JSON.parse(file)
|
|
41
|
+
: load(file);
|
|
42
|
+
const prefix = getPrefix(data);
|
|
43
|
+
traverse(json, {
|
|
44
|
+
callback(key, message) {
|
|
45
|
+
translationJson[key] = message;
|
|
46
|
+
},
|
|
47
|
+
prefix,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Do nothing
|
|
52
|
+
}
|
|
53
|
+
return translationJson;
|
|
54
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function sortTranslations(translations) {
|
|
2
|
+
const translationsSorted = new Map();
|
|
3
|
+
translations.forEach((keyToData, locale) => {
|
|
4
|
+
const keyToDataSorted = new Map(Array.from(keyToData).sort());
|
|
5
|
+
translationsSorted.set(locale, keyToDataSorted);
|
|
6
|
+
});
|
|
7
|
+
return translationsSorted;
|
|
8
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { extractTranslations, sortTranslations, } from './merge-translation-files/index.js';
|
|
4
|
+
export function mergeTranslationFiles(translationFiles, options) {
|
|
5
|
+
const { config, projectRoot } = options;
|
|
6
|
+
const { buildOptions } = config;
|
|
7
|
+
const translations = new Map();
|
|
8
|
+
translationFiles.forEach((data, filePath) => {
|
|
9
|
+
const file = readFileSync(join(projectRoot, filePath), 'utf8');
|
|
10
|
+
const translationJson = extractTranslations(file, {
|
|
11
|
+
filePath,
|
|
12
|
+
namespaceKeysByDir: buildOptions.namespaceKeysByDir,
|
|
13
|
+
translationsDir: data.translationsDir,
|
|
14
|
+
});
|
|
15
|
+
const keyToData = translations.get(data.locale) ??
|
|
16
|
+
new Map();
|
|
17
|
+
for (const [key, message] of Object.entries(translationJson)) {
|
|
18
|
+
keyToData.set(key, {
|
|
19
|
+
filePath,
|
|
20
|
+
message,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
translations.set(data.locale, keyToData);
|
|
24
|
+
});
|
|
25
|
+
if (buildOptions.fallbackLocale) {
|
|
26
|
+
const fallbacksKeyToData = translations.get(buildOptions.fallbackLocale);
|
|
27
|
+
translations.forEach((keyToData, locale) => {
|
|
28
|
+
if (locale === buildOptions.fallbackLocale) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
fallbacksKeyToData.forEach((data, key) => {
|
|
32
|
+
if (!keyToData.has(key)) {
|
|
33
|
+
keyToData.set(key, data);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
translations.set(locale, keyToData);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return sortTranslations(translations);
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Locale = string;
|
|
2
|
+
type ProjectTranslationData = {
|
|
3
|
+
filePath: TranslationFilePath;
|
|
4
|
+
message: TranslationMessage;
|
|
5
|
+
};
|
|
6
|
+
type TranslationFilePath = string;
|
|
7
|
+
type TranslationJson = Record<TranslationKey, TranslationMessage>;
|
|
8
|
+
type TranslationKey = string;
|
|
9
|
+
type TranslationMessage = string;
|
|
10
|
+
type Project = {
|
|
11
|
+
translationFiles: Map<TranslationFilePath, {
|
|
12
|
+
isInternal: boolean;
|
|
13
|
+
locale: Locale;
|
|
14
|
+
translationsDir: string;
|
|
15
|
+
}>;
|
|
16
|
+
translations: Map<Locale, Map<TranslationKey, ProjectTranslationData>>;
|
|
17
|
+
};
|
|
18
|
+
export type { Locale, Project, ProjectTranslationData, TranslationFilePath, TranslationJson, TranslationKey, TranslationMessage, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { findTranslationFiles } from './translations/find-translation-files.js';
|
|
2
|
+
export { mergeTranslationFiles } from './translations/merge-translation-files.js';
|
|
3
|
+
export type { Project, ProjectTranslationData, TranslationJson, TranslationKey, } from './translations/types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ember-intl/utils",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Utilities for ember-intl packages",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ember-intl"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://ember-intl.github.io/ember-intl/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/ember-intl/ember-intl.git",
|
|
12
|
+
"directory": "packages/ember-intl-utils"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./*": {
|
|
22
|
+
"types": "./dist/*.d.ts",
|
|
23
|
+
"default": "./dist/*.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@codemod-utils/files": "^4.1.0",
|
|
31
|
+
"js-yaml": "^5.4.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@codemod-utils/tests": "^3.3.0",
|
|
35
|
+
"@shared-configs/eslint-config-node": "0.0.0",
|
|
36
|
+
"@shared-configs/prettier": "0.0.0",
|
|
37
|
+
"@shared-configs/typescript": "0.0.0",
|
|
38
|
+
"@types/js-yaml": "^4.0.9",
|
|
39
|
+
"@types/node": "^22.20.4",
|
|
40
|
+
"concurrently": "^10.0.5",
|
|
41
|
+
"eslint": "^10.11.0",
|
|
42
|
+
"prettier": "^3.9.9",
|
|
43
|
+
"typescript": "^6.0.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "22.* || >= 24"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "sh build.sh --production",
|
|
50
|
+
"format": "prettier . --cache --write",
|
|
51
|
+
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
|
|
52
|
+
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" && pnpm format",
|
|
53
|
+
"lint:format": "prettier . --cache --check",
|
|
54
|
+
"lint:js": "eslint . --cache",
|
|
55
|
+
"lint:js:fix": "eslint . --fix",
|
|
56
|
+
"lint:types": "tsc --noEmit",
|
|
57
|
+
"test": "sh build.sh --test && mt dist-for-testing --quiet"
|
|
58
|
+
}
|
|
6
59
|
}
|