Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add i18n module #575

Merged
merged 35 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dd6d28c
chore: Basic setup of I18n module, added class and useTranslation pro…
mayan-000 Mar 21, 2024
7b74004
feat: add i18n provider to extension
mayan-000 Mar 21, 2024
0bb6583
ref: make useTranslation a hook
mayan-000 Mar 21, 2024
1f32f67
feat: make useTranslation a context provider and distribute functions…
mayan-000 Mar 21, 2024
90dbbac
feat: add provider to extension and dashboard for adding test string
mayan-000 Mar 21, 2024
8fe4333
feat: handle substitutions in custom texts
mayan-000 Mar 21, 2024
f36621d
update config
mayan-000 Mar 21, 2024
2585c7c
Test substitutions in extension and dashboard
mayan-000 Mar 21, 2024
3c71ef5
ref: move files and move message passing to new file
mayan-000 Mar 22, 2024
aa78199
create a singleton class for common interface across all modules and …
mayan-000 Mar 25, 2024
e7f26ca
feat: add hook for extracting messages in cli dashboard
mayan-000 Mar 25, 2024
fc21d74
update artifacts script
mayan-000 Mar 25, 2024
0873f77
ref: move parse message function inside class
mayan-000 Mar 25, 2024
c6dffea
update definition
mayan-000 Mar 25, 2024
b2a0de9
fix: update checks for error handling
mayan-000 Mar 25, 2024
3c81f41
feat: add logic to find fallback locales if not present
mayan-000 Mar 27, 2024
30a32c2
Remove unrequired dependencies
mayan-000 Mar 28, 2024
64e9ef0
ref: move fetching function to i18n module
mayan-000 Mar 29, 2024
fe711b3
update types
mayan-000 Mar 29, 2024
a0fc2e9
feat: update scripts to implement i18n in open cookie db and rws data
mayan-000 Apr 2, 2024
1e147a2
update condition to use i18n
mayan-000 Apr 2, 2024
66751fc
Merge branch 'develop' into feat/add-i18n
mayan-000 Apr 5, 2024
7948523
Move _locales folder in i18n package
mayan-000 Apr 5, 2024
b0e76b0
update _locale config
mayan-000 Apr 5, 2024
3a5088c
create package based messages file and update configs
mayan-000 Apr 5, 2024
9af623f
add script to merge messages json files
mayan-000 Apr 5, 2024
ed7640c
Comment the I18n usage across packages
mayan-000 Apr 8, 2024
fde3b29
Remove ext, webpack config and dependencies
mayan-000 Apr 8, 2024
c73235a
Merge branch 'develop' of github.com:GoogleChromeLabs/ps-analysis-too…
mohdsayed Apr 9, 2024
97eeed3
Add missing JSDocs and comments
mohdsayed Apr 9, 2024
eaf4a20
Small refactoring
mohdsayed Apr 9, 2024
bb8c7a0
Small refactoring
mohdsayed Apr 9, 2024
d25963e
remove comments
mayan-000 Apr 9, 2024
df99a7c
test: add i18n callbacks tests
mayan-000 Apr 9, 2024
dd7dbf1
remove commented code
mayan-000 Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16,794 changes: 360 additions & 16,434 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"prepare": "husky install",
"cookie-db:update": "node scripts/update-cookie-db.cjs",
"rws-json:update": "node scripts/update-rws-json.cjs",
"merge-i18n-messages": "node packages/i18n/scripts/merge-messages.cjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"start": "npm install && npm run dev"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"esModuleInterop": true,
"moduleResolution": "node"
},
"references": [{ "path": "../common" }]
"references": [{ "path": "../common" }, { "path": "../i18n" }]
}
6 changes: 6 additions & 0 deletions packages/i18n/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"no-console": "off"
},
"ignorePatterns": ["dist/**", "dist-types/**"]
}
5 changes: 5 additions & 0 deletions packages/i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# i18n

## Description

A package that handles internationalization and localization.
16 changes: 16 additions & 0 deletions packages/i18n/_locales/messages/en/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"testString": {
"message": "Testing $details$ with $format$",
"description": "This is a test string",
"placeholders": {
"details": {
"content": "$2",
"example": "Details of the test"
},
"format": {
"content": "$1",
"example": "Format of the test"
}
}
}
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
30 changes: 30 additions & 0 deletions packages/i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@ps-analysis-tool/i18n",
"version": "0.6.0",
"description": "A package that handles internationalization and localization.",
"main": "./dist/index.js",
"types": "./dist-types/index.d.ts",
"source": "./src/index.ts",
"customExports": {
".": {
"default": "./src/index.ts"
}
},
"scripts": {
"build": "tsc --build",
"dev": "tsc-watch --build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/GoogleChromeLabs/ps-analysis-tool"
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/GoogleChromeLabs/ps-analysis-tool/issues"
},
"homepage": "https://github.com/GoogleChromeLabs/ps-analysis-tool",
"dependencies": {
"intl-messageformat": "^10.5.11"
}
}
55 changes: 55 additions & 0 deletions packages/i18n/scripts/merge-messages.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* External dependencies.
*/
const fs = require('fs');

const PACKAGES = [
'cli',
'cli-dashboard',
'common',
'design-system',
'extension',
];
const COMMON_PATH = 'packages/i18n/_locales';
const TARGET = `${COMMON_PATH}/messages/en/messages.json`;

const main = () => {
fs.writeFileSync(TARGET, '{}');

const messages = {};

PACKAGES.forEach((pkg) => {
const path = `${COMMON_PATH}/packages/${pkg}/messages.json`;
const data = fs.readFileSync(path, 'utf8') || '{}';
const parsed = JSON.parse(data);

Object.entries(parsed).forEach(([key, value]) => {
if (messages[key]) {
throw new Error(
`Duplicate key: ${key}, found in ${pkg}. Please resolve this conflict before continuing.`
);
}

messages[key] = value;
});
});

fs.writeFileSync(TARGET, JSON.stringify(messages, null, 2));
};

main();
177 changes: 177 additions & 0 deletions packages/i18n/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* External dependencies.
*/
import { existsSync, readFileSync } from 'fs';
import { IntlMessageFormat } from 'intl-messageformat';

/**
* Class representing Internationalization (i18n) functionality.
*/
class I18n {
private messages: {
[key: string]: {
message: string;
description: string;
placeholders: {
[key: string]: {
content: string;
example: string;
};
};
};
} = {};

/**
* Initializes the messages object with the provided messages.
* @param {object} messages - The messages object containing translations.
*/
initMessages(messages = {}) {
this.messages = messages;
}

/**
* Creates an array of possible locale strings based on the provided locale.
* @param {string} locale - The locale string.
* @returns {string[]} An array of locale strings.
*/
private createLocaleArray(locale: string) {
return [locale, locale.split('-')[0], locale.split('_')[0], 'en'];
}

/**
* Asynchronously loads messages data for the dashboard.
* @param {string} locale - The locale string.
* @returns {Promise<void>} A promise that resolves when messages are loaded.
*/
async loadDashboardMessagesData(locale: string) {
const localeArray = this.createLocaleArray(locale);

let idx = 0;

const fetchWithRetry = async () => {
if (idx >= localeArray.length) {
return;
}

try {
const response = await fetch(
`/_locales/${localeArray[idx]}/messages.json`
);

if (!response.ok) {
throw new Error(
`Failed to fetch messages for locale ${localeArray[idx]}`
);
}

const data = await response.json();

this.initMessages(data);
} catch (error) {
idx++;
await fetchWithRetry();
}
};

await fetchWithRetry();
}

/**
* Loads messages data for the CLI.
* @param {string} locale - The locale string.
*/
loadCLIMessagesData(locale: string) {
const localeArray = this.createLocaleArray(locale);

for (const _locale of localeArray) {
if (
existsSync(`packages/i18n/_locales/messages/${_locale}/messages.json`)
) {
const messages = JSON.parse(
readFileSync(
`packages/i18n/_locales/messages/${_locale}/messages.json`,
{
encoding: 'utf-8',
}
)
);

this.initMessages(messages);
break;
}
}
}

/**
* Retrieves a translated message for a given key.
* @param {string} key - The key of the message to retrieve.
* @param {string[]} [substitutions] - An array of substitution values for placeholders in the message.
* @param {boolean} [escapeLt] - Whether to escape '<' characters.
* @returns {string} The translated message.
*/
getMessage(key: string, substitutions?: string[], escapeLt?: boolean) {
if (typeof chrome !== 'undefined' && chrome?.i18n?.getMessage) {
// @ts-ignore - Outdated definition.
return chrome.i18n.getMessage(key, substitutions, {
escapeLt: Boolean(escapeLt),
});
}

return this._parseMessage(key, substitutions, escapeLt);
}

/**
* Parses a message with substitutions and placeholders.
* @param {string} key - The key of the message to parse.
* @param {string[]} [substitutions] - An array of substitution values for placeholders in the message.
* @param {boolean} [escapeLt] - Whether to escape '<' characters.
* @returns {string} The parsed message.
*/
private _parseMessage(
key: string,
substitutions?: string[],
escapeLt?: boolean
) {
const messageObj = this.messages?.[key];

if (!messageObj) {
return '';
}

const message = messageObj.message
.split('$')
.map((part, idx) => (idx % 2 ? `{${part}}` : part))
.join('');

const placeholders = Object.entries(messageObj.placeholders || {}).reduce<{
[key: string]: string;
}>((acc, [placeholderKey, val]) => {
const idx = Number(val.content.substring(1)) - 1;

acc[placeholderKey] = substitutions?.[idx] || '';

return acc;
}, {});

return new IntlMessageFormat(message, 'en', undefined, {
ignoreTag: escapeLt,
}).format(placeholders) as string;
}
}

export default new I18n();
17 changes: 17 additions & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { default as I18n } from './i18n';
Loading
Loading