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

Feat: plugin for intl #6863

Merged
merged 6 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
1 change: 1 addition & 0 deletions examples/with-intl/.browserslistrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
chrome 55
6 changes: 6 additions & 0 deletions examples/with-intl/ice.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from '@ice/app';
import intl from '@ice/plugin-intl';

export default defineConfig(() => ({
plugins: [intl()],
}));
23 changes: 23 additions & 0 deletions examples/with-intl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@examples/with-intl",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "ice start",
"build": "ice build"
},
"description": "",
"author": "",
"license": "MIT",
"dependencies": {
"@ice/app": "workspace:*",
"@ice/plugin-intl": "workspace:*",
"@ice/runtime": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2"
}
}
Binary file added examples/with-intl/public/favicon.ico
Binary file not shown.
8 changes: 8 additions & 0 deletions examples/with-intl/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineAppConfig } from 'ice';
import type { LocaleConfig } from '@ice/plugin-intl/types';

export default defineAppConfig(() => ({}));

export const locale: LocaleConfig = {
getLocale: () => 'en-US',
};
22 changes: 22 additions & 0 deletions examples/with-intl/src/document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Meta, Title, Links, Main, Scripts } from 'ice';

function Document() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
}

export default Document;
3 changes: 3 additions & 0 deletions examples/with-intl/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
font-size: 14px;
}
3 changes: 3 additions & 0 deletions examples/with-intl/src/locales/en-US.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
new: 'New',
};
3 changes: 3 additions & 0 deletions examples/with-intl/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"new": "新建"
}
10 changes: 10 additions & 0 deletions examples/with-intl/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { intl } from 'ice';

export default function Home() {
return (
<>
<h1>home</h1>
<button>{intl.formatMessage({ id: 'new' })}</button>
</>
);
}
1 change: 1 addition & 0 deletions examples/with-intl/src/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="@ice/app/types" />
32 changes: 32 additions & 0 deletions examples/with-intl/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "esnext",
"target": "es6",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"rootDir": "./",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"ice": [".ice"]
}
},
"include": ["src", ".ice", "ice.config.*"],
"exclude": ["build", "public"]
}
1 change: 1 addition & 0 deletions packages/ice/templates/core/entry.server.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import routesManifest from './route-manifest.json';
import routesConfig from './routes-config.bundle.mjs';
<% if (dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%>
<% if (hydrate) {-%><%- runtimeOptions.imports %><% } -%>

<% if (!hydrate) {-%>
// Do not inject runtime modules when render mode is document only.
const commons = [];
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-intl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @ice/plugin-intl

## 1.0.0

- Initial release
73 changes: 73 additions & 0 deletions packages/plugin-intl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# @ice/plugin-intl

`@ice/plugin-intl` is a ice.js plugin. It provides a simple way to add internationalization support to your application.

> `@ice/plugin-intl` is based on `react-intl`.

## Install

```bash
$ npm i @ice/plugin-intl --save-dev
```

## Usage

Define the plugin in `ice.config.mts`:

```ts
import { defineConfig } from '@ice/app';
import intl from '@ice/plugin-intl';

export default defineConfig({
plugins: [
intl(),
],
});
```

Define locale config in `src/app.ts`:

```ts
import { defineAppConfig } from 'ice';
import type { LocaleConfig } from '@ice/plugin-intl/types';

export default defineAppConfig(() => ({}));

export const locale: LocaleConfig = {
// Cutomize getLocale method and other options supported by react-intl.
getLocale: () => 'en-US',
};
```

## Locales

Locales are defined in the `src/locales` directory. Each locale is defined in a separate file, with the locale name as the file name. For example, `en-US.ts`:

```ts
export default {
'app.title': 'My Application',
'app.welcome': 'Welcome to my application!',
};
```

Use the `useIntl` hook to access the current locale:

```tsx
import { useIntl } from 'ice';

export default function Home() {
const intl = useIntl();
console.log(intl.formatMessage({ id: 'new' }));
return <h1>home</h1>;
}
```

Use the `intl` function to access the current locale:

```tsx
import { intl } from 'ice';

function alertMessage() {
alert(intl.formatMessage({ id: 'app.welcome' }));
}
```
42 changes: 42 additions & 0 deletions packages/plugin-intl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@ice/plugin-intl",
"version": "1.0.0",
"description": "react intl plugin for ice.js 3.",
"files": [
"esm",
"!esm/**/*.map",
"*.d.ts",
"templates"
],
"type": "module",
"main": "esm/index.js",
"module": "esm/index.js",
"types": "esm/index.d.ts",
"exports": {
".": "./esm/index.js",
"./runtime": "./esm/runtime.js",
"./types": "./esm/types.js"
},
"sideEffects": false,
"scripts": {
"watch": "tsc -w --sourceMap",
"build": "tsc"
},
"dependencies": {
"react-intl": "^6.0.0",
"fast-glob": "^3.3.2"
},
"devDependencies": {
"@ice/app": "^3.3.2",
"@ice/runtime": "^1.2.9",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0"
},
"repository": {
"type": "http",
"url": "https://github.com/alibaba/ice/tree/master/packages/plugin-intl"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/plugin-intl/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './esm/runtime';
74 changes: 74 additions & 0 deletions packages/plugin-intl/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as path from 'path';
import { fileURLToPath } from 'url';
import fg from 'fast-glob';
import type { Plugin } from '@ice/app/types';

const _dirname = path.dirname(fileURLToPath(import.meta.url));

const plugin: Plugin = () => ({
name: 'plugin-intl',
setup: ({ generator, context, createLogger, watch }) => {
const { rootDir } = context;
const logger = createLogger('plugin-intl');

const renderLocaleEntry = (localeFiles: string[]) => {
const locales = [];
let localeExport = [];
localeFiles.forEach((file) => {
const filename = path.basename(file, path.extname(file));
// `-` is not allowed in import specifier.
const specifier = filename.replace('-', '_');
locales.push(`import ${specifier} from '@/locales/${filename}';`);
localeExport.push(`'${filename}': ${specifier},`);
});

generator.addRenderFile(
path.join(_dirname, '../templates/locales.ts.ejs'),
'locales.ts',
{
localeImport: locales.join('\n'),
localeExport: localeExport.join('\n '),
},
);
};
const globRule = 'src/locales/*.{ts,js,json}';
// Glob all locale files, and generate runtime options.
const localeFiles = fg.sync(globRule, { cwd: rootDir });
if (localeFiles.length > 0) {
// Filter the entry of locale files.
const mainEntry = localeFiles.find((file) => file.match(/index\.(ts|js|json)$/));
let runtimeSource = '';
if (mainEntry) {
runtimeSource = `@/locales/${path.basename(mainEntry)}`;
} else {
// Create a locale entry file to export all locale files.
renderLocaleEntry(localeFiles);

// Add watch event for locale files added or removed.
watch.addEvent([/src\/locales/, (event) => {
if (event === 'unlink' || event === 'add') {
const files = fg.sync(globRule, { cwd: rootDir });
renderLocaleEntry(files);
}
}]);

runtimeSource = './locales';
}
generator.addRuntimeOptions({
source: runtimeSource,
specifier: 'localeMessages',
});
} else {
logger.warn('No locale files found, please check the `src/locales` folder.');
}

// Add intl export from ice.
generator.addExport({
specifier: ['useIntl', 'intl'],
source: '@ice/plugin-intl/runtime',
});
},
runtime: '@ice/plugin-intl/runtime',
});

export default plugin;
43 changes: 43 additions & 0 deletions packages/plugin-intl/src/runtime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import { createIntl, createIntlCache, RawIntlProvider, useIntl } from 'react-intl';
import type { IntlShape } from 'react-intl';
import type { RuntimePlugin } from '@ice/runtime/types';
import type { LocaleConfig } from './types.js';

interface RuntimeOptons {
localeMessages?: Record<string, Record<string, string>>;
}

const EXPORT_NAME = 'locale';
const cache = createIntlCache();
let intl: IntlShape = null;

const getDefaultLocale = () => {
return (typeof navigator !== 'undefined' && navigator.language) || 'zh-CN';
};
const runtime: RuntimePlugin<RuntimeOptons> = async ({
addProvider,
appContext,
}, runtimeOptions) => {
const { appExport } = appContext;
const exported = appExport[EXPORT_NAME];
const localeConfig: LocaleConfig = (typeof exported === 'function' ? await exported() : exported) || {};
const { getLocale, ...l } = localeConfig;
const locale = getLocale ? getLocale() : getDefaultLocale();

intl = createIntl({
...l,
messages: runtimeOptions.localeMessages?.[locale] || {},
locale: getLocale ? getLocale() : getDefaultLocale(),
}, cache);
addProvider(({ children }) => {
return <RawIntlProvider value={intl}>{children}</RawIntlProvider>;
});
};

export {
intl,
useIntl,
};

export default runtime;
7 changes: 7 additions & 0 deletions packages/plugin-intl/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { IntlConfig } from 'react-intl';

interface AdditionalConfig {
getLocale: () => string;
}

export type LocaleConfig = Partial<IntlConfig> & AdditionalConfig;
5 changes: 5 additions & 0 deletions packages/plugin-intl/templates/locales.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%- localeImport %>

export default {
<%- localeExport %>
};
Loading
Loading