Initial commit
This commit is contained in:
1
packages/i18n/.gitignore
vendored
Normal file
1
packages/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib/i18n.ts
|
||||
173
packages/i18n/README.md
Normal file
173
packages/i18n/README.md
Normal 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
1
packages/i18n/index.mts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/index.js';
|
||||
62
packages/i18n/lib/consts.ts
Normal file
62
packages/i18n/lib/consts.ts
Normal 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';
|
||||
34
packages/i18n/lib/i18n-dev.ts
Normal file
34
packages/i18n/lib/i18n-dev.ts
Normal 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));
|
||||
3
packages/i18n/lib/i18n-prod.ts
Normal file
3
packages/i18n/lib/i18n-prod.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { MessageKeyType } from './types.js';
|
||||
|
||||
export const t = (key: MessageKeyType, substitutions?: string | string[]) => chrome.i18n.getMessage(key, substitutions);
|
||||
4
packages/i18n/lib/index.ts
Normal file
4
packages/i18n/lib/index.ts
Normal 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;
|
||||
22
packages/i18n/lib/prepare-build.ts
Normal file
22
packages/i18n/lib/prepare-build.ts
Normal 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');
|
||||
})();
|
||||
38
packages/i18n/lib/set-related-locale-import.ts
Normal file
38
packages/i18n/lib/set-related-locale-import.ts
Normal 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');
|
||||
};
|
||||
12
packages/i18n/lib/types.ts
Normal file
12
packages/i18n/lib/types.ts
Normal 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;
|
||||
45
packages/i18n/locales/en/messages.json
Normal file
45
packages/i18n/locales/en/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
45
packages/i18n/locales/ko/messages.json
Normal file
45
packages/i18n/locales/ko/messages.json
Normal 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": "다시 실행합니다."
|
||||
}
|
||||
}
|
||||
30
packages/i18n/package.json
Normal file
30
packages/i18n/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
4
packages/i18n/prepare-build.tsconfig.json
Normal file
4
packages/i18n/prepare-build.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["lib/prepare-build.ts"]
|
||||
}
|
||||
8
packages/i18n/tsconfig.json
Normal file
8
packages/i18n/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/module",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["index.mts", "lib"]
|
||||
}
|
||||
Reference in New Issue
Block a user