@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.
Files changed (33) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +17 -2
  3. package/dist/config/find-user-config.d.ts +1 -0
  4. package/dist/config/find-user-config.js +13 -0
  5. package/dist/config/get-default-config.d.ts +2 -0
  6. package/dist/config/get-default-config.js +16 -0
  7. package/dist/config/get-user-config.d.ts +2 -0
  8. package/dist/config/get-user-config.js +45 -0
  9. package/dist/config/merge-configs.d.ts +2 -0
  10. package/dist/config/merge-configs.js +21 -0
  11. package/dist/config/types.d.ts +17 -0
  12. package/dist/config/types.js +1 -0
  13. package/dist/config.d.ts +5 -0
  14. package/dist/config.js +4 -0
  15. package/dist/lint-rules.d.ts +2 -0
  16. package/dist/lint-rules.js +5 -0
  17. package/dist/translations/find-translation-files.d.ts +3 -0
  18. package/dist/translations/find-translation-files.js +38 -0
  19. package/dist/translations/merge-translation-files/extract-translations.d.ts +8 -0
  20. package/dist/translations/merge-translation-files/extract-translations.js +54 -0
  21. package/dist/translations/merge-translation-files/index.d.ts +2 -0
  22. package/dist/translations/merge-translation-files/index.js +2 -0
  23. package/dist/translations/merge-translation-files/sort-translations.d.ts +2 -0
  24. package/dist/translations/merge-translation-files/sort-translations.js +8 -0
  25. package/dist/translations/merge-translation-files.d.ts +3 -0
  26. package/dist/translations/merge-translation-files.js +40 -0
  27. package/dist/translations/types.d.ts +18 -0
  28. package/dist/translations/types.js +1 -0
  29. package/dist/translations.d.ts +3 -0
  30. package/dist/translations.js +2 -0
  31. package/dist/types/index.d.ts +6 -0
  32. package/dist/types/index.js +1 -0
  33. 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
- # Temporary Holding Version
1
+ # @ember-intl/utils
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,2 @@
1
+ import type { Config } from './types.js';
2
+ export declare function getDefaultConfig(): Config;
@@ -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,2 @@
1
+ import type { UserConfig } from './types.js';
2
+ export declare function getUserConfig(projectRoot: string): Promise<undefined | UserConfig>;
@@ -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,2 @@
1
+ import type { Config, UserConfig } from './types.js';
2
+ export declare function mergeConfigs(config: Config, userConfig: undefined | UserConfig): Config;
@@ -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 {};
@@ -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,4 @@
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';
@@ -0,0 +1,2 @@
1
+ export declare const lintRules: readonly ["no-inconsistent-messages", "no-missing-keys", "no-unused-keys"];
2
+ export type LintRule = (typeof lintRules)[number];
@@ -0,0 +1,5 @@
1
+ export const lintRules = [
2
+ 'no-inconsistent-messages',
3
+ 'no-missing-keys',
4
+ 'no-unused-keys',
5
+ ];
@@ -0,0 +1,3 @@
1
+ import type { Options } from '../types/index.js';
2
+ import type { Project } from './types.js';
3
+ export declare function findTranslationFiles(options: Options): Project['translationFiles'];
@@ -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,2 @@
1
+ export * from './extract-translations.js';
2
+ export * from './sort-translations.js';
@@ -0,0 +1,2 @@
1
+ export * from './extract-translations.js';
2
+ export * from './sort-translations.js';
@@ -0,0 +1,2 @@
1
+ import type { Project } from '../types.js';
2
+ export declare function sortTranslations(translations: Project['translations']): Project['translations'];
@@ -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,3 @@
1
+ import type { Options } from '../types/index.js';
2
+ import type { Project } from './types.js';
3
+ export declare function mergeTranslationFiles(translationFiles: Project['translationFiles'], options: Options): Project['translations'];
@@ -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,2 @@
1
+ export { findTranslationFiles } from './translations/find-translation-files.js';
2
+ export { mergeTranslationFiles } from './translations/merge-translation-files.js';
@@ -0,0 +1,6 @@
1
+ import type { Config } from '../config.js';
2
+ export type Options = {
3
+ [key: string]: unknown;
4
+ config: Config;
5
+ projectRoot: string;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@ember-intl/utils",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }