Initial commit

This commit is contained in:
Aditya Gupta
2026-06-29 19:29:19 +05:30
committed by GitHub
commit aabb8986aa
292 changed files with 17976 additions and 0 deletions

1
packages/i18n/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
lib/i18n.ts

173
packages/i18n/README.md Normal file
View File

@@ -0,0 +1,173 @@
# I18n Package
This package provides a set of tools to help you internationalize your Chrome Extension.
https://developer.chrome.com/docs/extensions/reference/api/i18n
## Installation
If you want to use the i18n translation function in each pages, you need to add the following to the package.json file.
```json
{
"dependencies": {
"@extension/i18n": "workspace:*"
}
}
```
Then run the following command to install the package.
```bash
pnpm install
```
## Manage translations
You can manage translations in the `locales` directory.
`locales/en/messages.json`
```json
{
"helloWorld": {
"message": "Hello, World!"
}
}
```
`locales/ko/messages.json`
```json
{
"helloWorld": {
"message": "안녕하세요, 여러분!"
}
}
```
## Add a new language
Create folder inside `locales` with name from [languages](https://developer.chrome.com/docs/extensions/reference/api/i18n?hl=pl#support_multiple_languages), which need include `message.json` file.
## Usage
### Translation function
Just import the `t` function and use it to translate the key.
```typescript
import { t } from '@extension/i18n';
console.log(t('loading')); // Loading...
```
```typescript jsx
import { t } from '@extension/i18n';
const Component = () => {
return (
<button>
{t('toggleTheme')} // Toggle Theme
</button>
);
};
```
### Placeholders
If you want to use placeholders, you can use the following format.
> For more information, see the [Message Placeholders](https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders) section.
`locales/en/messages.json`
```json
{
"greeting": {
"description": "Greeting message",
"message": "Hello, My name is $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
}
}
},
"hello": {
"description": "Placeholder example",
"message": "Hello $1"
}
}
```
`locales/ko/messages.json`
```json
{
"greeting": {
"description": "인사 메시지",
"message": "안녕하세요, 제 이름은 $NAME$입니다.",
"placeholders": {
"name": {
"content": "$1",
"example": "서종학"
}
}
},
"hello": {
"description": "Placeholder 예시",
"message": "안녕 $1"
}
}
```
If you want to replace the placeholder, you can pass the value as the second argument.
Function `t` has exactly the same interface as the `chrome.i18n.getMessage` function.
```typescript
import { t } from '@extension/i18n';
console.log(t('greeting', 'John Doe')); // Hello, My name is John Doe
console.log(t('greeting', ['John Doe'])); // Hello, My name is John Doe
console.log(t('hello')); // Hello
console.log(t('hello', 'World')); // Hello World
console.log(t('hello', ['World'])); // Hello World
```
### Locale setting on development
If you want to enforce displaying specific language, you need to set `CEB_DEV_LOCALE` in `.env` file (work only for development).
### Type Safety
When you forget to add a key to all language's `messages.json` files, you will get a Typescript error.
`locales/en/messages.json`
```json
{
"hello": {
"message": "Hello World!"
}
}
```
`locales/ko/messages.json`
```json
{
"helloWorld": {
"message": "안녕하세요, 여러분!"
}
}
```
```typescript
import { t } from '@extension/i18n';
// Error: TS2345: Argument of type "hello" is not assignable to parameter of type
console.log(t('hello'));
```

1
packages/i18n/index.mts Normal file
View File

@@ -0,0 +1 @@
export * from './lib/index.js';

View File

@@ -0,0 +1,62 @@
/**
* @url https://developer.chrome.com/docs/extensions/reference/api/i18n#support_multiple_languages
*/
export const SUPPORTED_LANGUAGES = {
ar: 'Arabic',
am: 'Amharic',
bg: 'Bulgarian',
bn: 'Bengali',
ca: 'Catalan',
cs: 'Czech',
da: 'Danish',
de: 'German',
el: 'Greek',
en: 'English',
en_AU: 'English (Australia)',
en_GB: 'English (Great Britain)',
en_US: 'English (USA)',
es: 'Spanish',
es_419: 'Spanish (Latin America and Caribbean)',
et: 'Estonian',
fa: 'Persian',
fi: 'Finnish',
fil: 'Filipino',
fr: 'French',
gu: 'Gujarati',
he: 'Hebrew',
hi: 'Hindi',
hr: 'Croatian',
hu: 'Hungarian',
id: 'Indonesian',
it: 'Italian',
ja: 'Japanese',
kn: 'Kannada',
ko: 'Korean',
lt: 'Lithuanian',
lv: 'Latvian',
ml: 'Malayalam',
mr: 'Marathi',
ms: 'Malay',
nl: 'Dutch',
no: 'Norwegian',
pl: 'Polish',
pt_BR: 'Portuguese (Brazil)',
pt_PT: 'Portuguese (Portugal)',
ro: 'Romanian',
ru: 'Russian',
sk: 'Slovak',
sl: 'Slovenian',
sr: 'Serbian',
sv: 'Swedish',
sw: 'Swahili',
ta: 'Tamil',
te: 'Telugu',
th: 'Thai',
tr: 'Turkish',
uk: 'Ukrainian',
vi: 'Vietnamese',
zh_CN: 'Chinese (China)',
zh_TW: 'Chinese (Taiwan)',
} as const;
export const I18N_FILE_PATH = './lib/i18n.ts';

View File

@@ -0,0 +1,34 @@
// IT WILL BE ADJUSTED TO YOUR LANGUAGE DURING BUILD TIME, DON'T MOVE BELOW IMPORT TO OTHER LINE
import localeJSON from '../locales/en/messages.json' with { type: 'json' };
import type { I18nValueType, LocalesJSONType } from './types.js';
const translate = (key: keyof LocalesJSONType, substitutions?: string | string[]) => {
const localeValues = localeJSON[key] as I18nValueType;
let message = localeValues.message;
/**
* This is a placeholder replacement logic. But it's not perfect.
* It just imitates the behavior of the Chrome extension i18n API.
* Please check the official document for more information And double-check the behavior on production build.
*
* @url https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders
*/
if (localeValues.placeholders) {
Object.entries(localeValues.placeholders).forEach(([key, { content }]) => {
if (content) {
message = message.replace(new RegExp(`\\$${key}\\$`, 'gi'), content);
}
});
}
if (!substitutions) {
return message;
} else if (Array.isArray(substitutions)) {
return substitutions.reduce((acc, cur, idx) => acc.replace(`$${idx++}`, cur), message);
}
return message.replace(/\$(\d+)/, substitutions);
};
const removePlaceholder = (message: string) => message.replace(/\$\d+/g, '');
export const t = (...args: Parameters<typeof translate>) => removePlaceholder(translate(...args));

View File

@@ -0,0 +1,3 @@
import type { MessageKeyType } from './types.js';
export const t = (key: MessageKeyType, substitutions?: string | string[]) => chrome.i18n.getMessage(key, substitutions);

View File

@@ -0,0 +1,4 @@
import { t as t_dev_or_prod } from './i18n.js';
import type { t as t_dev } from './i18n-dev.js';
export const t = t_dev_or_prod as unknown as typeof t_dev;

View File

@@ -0,0 +1,22 @@
import setRelatedLocaleImports from './set-related-locale-import.js';
import { IS_DEV } from '@extension/env';
import { cpSync, existsSync, mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
(() => {
const i18nPath = IS_DEV ? 'lib/i18n-dev.ts' : 'lib/i18n-prod.ts';
cpSync(i18nPath, resolve('lib', 'i18n.ts'));
const outDir = resolve(import.meta.dirname, '..', '..', '..', '..', 'dist');
if (!existsSync(outDir)) {
mkdirSync(outDir);
}
const localePath = resolve(outDir, '_locales');
cpSync(resolve('locales'), localePath, { recursive: true });
if (IS_DEV) {
setRelatedLocaleImports();
}
console.log('I18n build complete');
})();

View File

@@ -0,0 +1,38 @@
import { I18N_FILE_PATH } from './consts.js';
import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { SupportedLanguagesKeysType, SupportedLanguagesWithoutRegionKeysType } from './types.js';
export default () => {
const locale = Intl.DateTimeFormat().resolvedOptions().locale.replace('-', '_') as SupportedLanguagesKeysType;
const localeWithoutRegion = locale.split('_')[0] as SupportedLanguagesWithoutRegionKeysType;
const localesDir = resolve(import.meta.dirname, '..', '..', 'locales');
const readLocalesFolder = readdirSync(localesDir);
const implementedLocales = readLocalesFolder.map(innerDir => {
if (lstatSync(resolve(localesDir, innerDir)).isDirectory()) {
return innerDir;
}
return;
});
const i18nFileSplitContent = readFileSync(I18N_FILE_PATH, 'utf-8').split('\n');
if (process.env['CEB_DEV_LOCALE']) {
i18nFileSplitContent[1] = `import localeJSON from '../locales/${process.env['CEB_DEV_LOCALE']}/messages.json' with { type: 'json' };`;
} else {
if (implementedLocales.includes(locale)) {
i18nFileSplitContent[1] = `import localeJSON from '../locales/${locale}/messages.json' with { type: 'json' };`;
} else if (implementedLocales.includes(localeWithoutRegion)) {
i18nFileSplitContent[1] = `import localeJSON from '../locales/${localeWithoutRegion}/messages.json' with { type: 'json' };`;
} else {
i18nFileSplitContent[1] = `import localeJSON from '../locales/en/messages.json' with { type: 'json' };`;
}
}
// Join lines back together
const updatedI18nFile = i18nFileSplitContent.join('\n');
writeFileSync(I18N_FILE_PATH, updatedI18nFile, 'utf-8');
};

View File

@@ -0,0 +1,12 @@
import enMessage from '../locales/en/messages.json' with { type: 'json' };
import type { SUPPORTED_LANGUAGES } from './consts.js';
export type SupportedLanguagesKeysType = keyof typeof SUPPORTED_LANGUAGES;
export type SupportedLanguagesWithoutRegionKeysType = Exclude<SupportedLanguagesKeysType, `${string}_${string}`>;
export type I18nValueType = {
message: string;
placeholders?: Record<string, { content?: string; example?: string }>;
};
export type MessageKeyType = keyof typeof enMessage;
export type LocalesJSONType = typeof enMessage;

View File

@@ -0,0 +1,45 @@
{
"extensionDescription": {
"description": "Extension description",
"message": "Chrome extension boilerplate developed with Vite, React and Typescript"
},
"extensionName": {
"description": "Extension name",
"message": "Chrome extension boilerplate"
},
"toggleTheme": {
"message": "Toggle theme"
},
"injectButton": {
"message": "Click to inject Content Scripts"
},
"greeting": {
"description": "Greeting message",
"message": "Hello, My name is $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
}
}
},
"hello": {
"description": "Placeholder example",
"message": "Hello $1"
},
"displayErrorInfo": {
"message": "Error occur"
},
"displayErrorDescription": {
"message": "Sorry, something went wrong while loading the page."
},
"displayErrorDetailsInfo": {
"message": "Error details:"
},
"displayErrorUnknownErrorInfo": {
"message": "Unknown error"
},
"displayErrorReset": {
"message": "Run Again"
}
}

View File

@@ -0,0 +1,45 @@
{
"extensionDescription": {
"description": "Extension description",
"message": "React, Typescript, Vite를 사용한 크롬 익스텐션 보일러플레이트입니다."
},
"extensionName": {
"description": "Extension name",
"message": "크롬 익스텐션 보일러플레이트"
},
"toggleTheme": {
"message": "테마 변경"
},
"injectButton": {
"message": "콘텐츠 스크립트를 삽입하려면 클릭하세요."
},
"greeting": {
"description": "인사 메시지",
"message": "안녕하세요, 제 이름은 $NAME$입니다.",
"placeholders": {
"name": {
"content": "$1",
"example": "홍길동"
}
}
},
"hello": {
"description": "Placeholder 예시",
"message": "안녕하세요, $1님."
},
"displayErrorInfo": {
"message": "오류 발생"
},
"displayErrorDescription": {
"message": "죄송합니다. 페이지를 로드하는 중 문제가 발생했습니다."
},
"displayErrorDetailsInfo": {
"message": "오류 세부 정보:"
},
"displayErrorUnknownErrorInfo": {
"message": "알 수 없는 오류"
},
"displayErrorReset": {
"message": "다시 실행합니다."
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "@extension/i18n",
"version": "0.5.0",
"description": "chrome extension - internationalization",
"type": "module",
"private": true,
"sideEffects": false,
"files": [
"dist/**"
],
"main": "dist/index.mjs",
"types": "index.mts",
"scripts": {
"clean:bundle": "rimraf dist",
"clean:node_modules": "pnpx rimraf node_modules",
"clean:turbo": "rimraf .turbo",
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
"ready": "tsc -b prepare-build.tsconfig.json && node --env-file=../../.env dist/lib/prepare-build.js && tsc -b",
"lint": "eslint .",
"lint:fix": "pnpm lint --fix",
"format": "prettier . --write --ignore-path ../../.prettierignore",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@extension/env": "workspace:*"
},
"devDependencies": {
"@extension/tsconfig": "workspace:*"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["lib/prepare-build.ts"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@extension/tsconfig/module",
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist"
},
"include": ["index.mts", "lib"]
}