├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── LOG.md ├── README.md ├── output ├── package-lock.json ├── package.json ├── public ├── favicon.svg └── manifest.json ├── src ├── components │ ├── header │ │ ├── header.css │ │ └── header.tsx │ ├── icons │ │ └── qwik.tsx │ └── router-head │ │ └── router-head.tsx ├── entry.dev.tsx ├── entry.express.tsx ├── entry.preview.tsx ├── entry.ssr.tsx ├── global.css ├── i18n.ts ├── locale │ ├── message.en.json │ ├── message.fr.json │ ├── message.sk.json │ └── message.sp.json ├── root.tsx └── routes │ ├── [...locale] │ ├── blog │ │ ├── [id] │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.tsx │ └── layout.tsx │ ├── index.ts │ └── service-worker.ts ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:qwik/recommended", 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | tsconfigRootDir: __dirname, 16 | project: ["./tsconfig.json"], 17 | ecmaVersion: 2021, 18 | sourceType: "module", 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | }, 23 | plugins: ["@typescript-eslint"], 24 | rules: { 25 | "@typescript-eslint/no-explicit-any": "off", 26 | "@typescript-eslint/explicit-module-boundary-types": "off", 27 | "@typescript-eslint/no-inferrable-types": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-empty-interface": "off", 30 | "@typescript-eslint/no-namespace": "off", 31 | "@typescript-eslint/no-empty-function": "off", 32 | "@typescript-eslint/no-this-alias": "off", 33 | "@typescript-eslint/ban-types": "off", 34 | "@typescript-eslint/ban-ts-comment": "off", 35 | "prefer-spread": "off", 36 | "no-case-declarations": "off", 37 | "no-console": "off", 38 | "@typescript-eslint/no-unused-vars": ["error"], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /dist 3 | /lib 4 | /lib-types 5 | /server 6 | 7 | # Development 8 | node_modules 9 | 10 | # Cache 11 | .cache 12 | .mf 13 | .vscode 14 | .rollup.cache 15 | tsconfig.tsbuildinfo 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | pnpm-debug.log* 24 | lerna-debug.log* 25 | 26 | # Editor 27 | !.vscode/extensions.json 28 | .idea 29 | .DS_Store 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | 36 | # Yarn 37 | .yarn/* 38 | !.yarn/releases 39 | .history 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Files Prettier should not format 2 | **/*.log 3 | **/.DS_Store 4 | *. 5 | dist 6 | node_modules 7 | -------------------------------------------------------------------------------- /LOG.md: -------------------------------------------------------------------------------- 1 | 1. add `yarn add @angular/localize` 2 | 2. import `import {} from "@angular/localize/init";` 3 | - not sure why need to import form `{}` 4 | 5 | --- 6 | 7 | ```typescript 8 | http://yourserver/en/[..path] 9 | http://sk.yourserver/[..path] 10 | 11 | const fn () => { 12 | useWatch$(() => { 13 | state.done = $localize`Done`; 14 | }); 15 | return $localize`You have ${count} emails since ${date}!` 16 | } 17 | 18 | try { 19 | $loalize.transaltion = ...; 20 | fn(); 21 | } finally { 22 | $loalize.transaltion = null; 23 | } 24 | $localize`Since ${date} you have ${count}!` 25 | ``` 26 | 27 | 123, 1/1/2022 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i18n demo for Qwik 2 | 3 | Qwik is unique in that it has fine-grained lazy loading of code. The classical way to do translation is at runtime by looking up the translation strings in the translation map. This is not conducive with lazy loading because it requires that the translations be eagerly loaded, defeating the fine-grained lazy loading of Qwik. 4 | 5 | ## Overview 6 | 7 | There are two ways to do translation: 8 | 9 | 1. **Runtime**: Translation is performed at runtime by looking up the translation strings in the translation map. 10 | - PROS: 11 | - No build step. 12 | - Easy way to test the application in development. 13 | - CONS: 14 | - Translations strings must be eagerly loaded. 15 | - Each string is in triplicate. 1) original string, 2) translation string, 3) key to lookup translation string. 16 | 2. **Compile time**: Translation is performed as part of the build step. 17 | - PROS: 18 | - Translated strings are inlined into the application. No need to load or look them up at runtime. 19 | - Because the strings are inlined, they can be lazy-loaded with application code. 20 | - CONS: 21 | - Requires a build step. 22 | - User can't change the language without a page refresh. (Or have mixed languages on the same page.) 23 | 24 | We think that the best approach is to use a hybrid approach. 25 | 26 | 1. During development, use runtime translation to make the development easy, and not require extra build steps. 27 | 2. During production use: 28 | - compile time translation for code sent to the browser. It is important for the user experience to be fast only the compile-time approach can provide the best user experience. 29 | - runtime translation for server code. Because lazy loading is not of concern on the server, a simpler runtime approach is used. This results in a single binary that needs to be deployed to the server. 30 | 31 | ## `$localize` (by Angular) 32 | 33 | The translation system is based on the `$localize` system from [Angular](https://angular.io/api/localize/init/$localize). The translations can be extracted in `xmb`, `xlf`, `xlif`, `xliff`, `xlf2`, `xlif2`, `xliff2`, and `json` formats. 34 | 35 | > NOTE: The `$localize` system is a compile-time translation system and is completely removed from the final output. `$localize` is a sub-project of Angular, and including its usage does not mean that Angular is used for rendering of applications. 36 | 37 | ### Marking string for translation 38 | 39 | Any string can be marked for translation by using the `$localize` template function like so: 40 | 41 | ```typescript 42 | export default component$((props: { name: string }) => { 43 | return {$localize`Hello ${props.name}!`}; 44 | }); 45 | ``` 46 | 47 | ### Extracting string for translation 48 | 49 | The first step in translation is to build the application. Once the artifacts are build the strings can be extracted for translation. 50 | 51 | ```bash 52 | npm run build.client 53 | npm run i18n-extract 54 | ``` 55 | 56 | The result of the commands is `src/locale/message.en.json`. 57 | 58 | ### Translating strings 59 | 60 | Take the resulting string and send them for translation. Produce a file for each language. For example: 61 | 62 | ```bash 63 | src/locale/message.en.json # Original strings 64 | src/locale/message.fr.json 65 | src/locale/message.sp.json 66 | ``` 67 | 68 | ### Inline strings 69 | 70 | The strings need to be inlined into the application. This is done automatically as part of the build.client process. 71 | 72 | ```bash 73 | npm run build.client 74 | ``` 75 | 76 | The result of this command is that the browser chunks are generated once for each locale. For example: 77 | 78 | ```bash 79 | dist/build/q-*.js # Original chunks 80 | dist/build/en/q-*.js 81 | dist/build/fr/q-*.js 82 | dist/build/sp/q-*.js 83 | ``` 84 | 85 | ## Development mode 86 | 87 | ```bash 88 | npm run dev 89 | ``` 90 | 91 | Navigate to `http://localhost:5173`. The resulting language should match your browser language. It will pick `sk` if it can't detect a language, this can happen when you run under StackBlitz for example. You can also override the language by adding `?locale=fr` to the URL. 92 | 93 | ## Building the application 94 | 95 | Here are the steps to build the application for production. 96 | 97 | ```sh 98 | npm run build.client && npm run build.server && npm run i18n-translate && npm run serve 99 | ``` 100 | 101 | --- 102 | 103 | # Qwik App ⚡️ 104 | 105 | - [Qwik Docs](https://qwik.builder.io/) 106 | - [Discord](https://qwik.builder.io/chat) 107 | - [Qwik Github](https://github.com/BuilderIO/qwik) 108 | - [@QwikDev](https://twitter.com/QwikDev) 109 | - [Vite](https://vitejs.dev/) 110 | - [Partytown](https://partytown.builder.io/) 111 | - [Mitosis](https://github.com/BuilderIO/mitosis) 112 | - [Builder.io](https://www.builder.io/) 113 | 114 | --- 115 | 116 | ## Project Structure 117 | 118 | Inside of you project, you'll see the following directories and files: 119 | 120 | ``` 121 | 122 | ├── public/ 123 | │ └── ... 124 | └── src/ 125 | ├── components/ 126 | │ └── ... 127 | └── routes/ 128 | └── ... 129 | 130 | ``` 131 | 132 | - `src/routes`: Provides the directory based routing, which can include a hierarchy of `layout.tsx` layout files, and `index.tsx` files as the page. Additionally, `index.ts` files are endpoints. Please see the [routing docs](https://qwik.builder.io/qwikcity/routing/overview/) for more info. 133 | 134 | - `src/components`: Recommended directory for components. 135 | 136 | - `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info. 137 | 138 | ## Add Integrations 139 | 140 | Use the `npm run qwik add` command to add other integrations. Some examples of integrations include as a Cloudflare, Netlify or Vercel server, and the Static Site Generator (SSG). 141 | 142 | ``` 143 | 144 | npm run qwik add 145 | 146 | ``` 147 | 148 | ## Development 149 | 150 | Development mode uses [Vite's development server](https://vitejs.dev/). For Qwik during development, the `dev` command will also server-side render (SSR) the output. The client-side development modules loaded by the browser. 151 | 152 | ``` 153 | 154 | npm run dev 155 | 156 | ``` 157 | 158 | > Note: during dev mode, Vite will request many JS files, which does not represent a Qwik production build. 159 | 160 | ## Preview 161 | 162 | The preview command will create a production build of the client modules, production build of `src/entry.preview.tsx`, and create a local server. The preview server is only for convenience to locally preview a production build, but it should not be used as a production server. 163 | 164 | ``` 165 | 166 | npm run preview 167 | 168 | ``` 169 | 170 | ## Production 171 | 172 | The production build should generate the client and server modules by running both client and server build commands. Additionally, the build command will use Typescript run a type check on the source. 173 | 174 | ``` 175 | 176 | npm run build 177 | 178 | ``` 179 | 180 | ## Express Server 181 | 182 | This app has a minimal [Express server](https://expressjs.com/) implementation. After running a full build, you can preview the build using the command: 183 | 184 | ``` 185 | 186 | npm run serve 187 | 188 | ``` 189 | 190 | Then visit [http://localhost:8080/](http://localhost:8080/) 191 | 192 | ``` 193 | 194 | ``` 195 | 196 | ``` 197 | 198 | ``` 199 | -------------------------------------------------------------------------------- /output: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-qwik-basic-starter", 3 | "description": "Recommended for your first Qwik app", 4 | "engines": { 5 | "node": ">=15.0.0" 6 | }, 7 | "private": true, 8 | "type": "module", 9 | "scripts": { 10 | "build": "qwik build", 11 | "build.client": "vite build && npm run i18n-translate", 12 | "build.preview": "vite build --ssr src/entry.preview.tsx", 13 | "build.server": "vite build --ssr src/entry.express.tsx", 14 | "build.types": "tsc --incremental --noEmit", 15 | "dev": "vite --mode ssr", 16 | "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", 17 | "fmt": "prettier --write .", 18 | "fmt.check": "prettier --check .", 19 | "i18n-extract": "node_modules/.bin/localize-extract -s \"dist/build/*.js\" -f json -o src/locale/message.en.json", 20 | "i18n-translate": "node_modules/.bin/localize-translate -s \"*.js\" -t src/locale/message.*.json -o dist/build/{{LOCALE}} -r ./dist/build", 21 | "lint": "eslint \"src/**/*.ts*\"", 22 | "preview": "qwik build preview && vite preview --open", 23 | "serve": "node server/entry.express", 24 | "start": "vite --open --mode ssr", 25 | "qwik": "qwik" 26 | }, 27 | "devDependencies": { 28 | "@angular/compiler": "^16.1.5", 29 | "@angular/compiler-cli": "^16.1.5", 30 | "@builder.io/qwik": "^1.2.6", 31 | "@builder.io/qwik-city": "^1.2.6", 32 | "@types/compression": "^1.7.2", 33 | "@types/eslint": "^8.44.0", 34 | "@types/express": "^4.17.17", 35 | "@types/node": "latest", 36 | "@typescript-eslint/eslint-plugin": "^6.1.0", 37 | "@typescript-eslint/parser": "^6.1.0", 38 | "eslint": "^8.45.0", 39 | "eslint-plugin-qwik": "^1.2.6", 40 | "express": "^4.18.2", 41 | "node-fetch": "^3.3.1", 42 | "prettier": "^3.0.0", 43 | "typescript": "^5.1.6", 44 | "vite": "^4.4.4", 45 | "vite-tsconfig-paths": "^4.2.0" 46 | }, 47 | "dependencies": { 48 | "@angular/localize": "^16.1.5", 49 | "compression": "^1.7.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "qwik-i18n", 4 | "short_name": "qwik-i18n", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#fff", 8 | "description": "Prototype for $localize use in Qwik" 9 | } 10 | -------------------------------------------------------------------------------- /src/components/header/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | background: var(--qwik-purple); 3 | } 4 | header { 5 | display: flex; 6 | background: white; 7 | border-bottom: 10px solid var(--qwik-dark-purple); 8 | } 9 | 10 | header .logo a { 11 | display: inline-block; 12 | padding: 10px 10px 7px 20px; 13 | } 14 | 15 | header ul { 16 | margin: 0; 17 | padding: 3px 10px 0 0; 18 | list-style: none; 19 | flex: 1; 20 | text-align: right; 21 | } 22 | 23 | header li { 24 | display: inline-block; 25 | margin: 0; 26 | padding: 15px 10px; 27 | } 28 | 29 | header li a { 30 | text-decoration: none; 31 | } 32 | 33 | header li a:hover { 34 | text-decoration: underline; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/header/header.tsx: -------------------------------------------------------------------------------- 1 | import { component$, useStylesScoped$ } from "@builder.io/qwik"; 2 | import { QwikLogo } from "../icons/qwik"; 3 | import styles from "./header.css?inline"; 4 | import { Link, RouteLocation, useLocation } from "@builder.io/qwik-city"; 5 | 6 | const LocaleLink = ({ 7 | locale, 8 | location, 9 | }: { 10 | locale: string; 11 | location: RouteLocation; 12 | }) => ( 13 |
  • 14 | {locale === location.params.locale ? ( 15 |
    {locale}
    16 | ) : ( 17 | 22 | {locale} 23 | 24 | )} 25 |
  • 26 | ); 27 | 28 | export default component$(() => { 29 | const location = useLocation(); 30 | useStylesScoped$(styles); 31 | 32 | return ( 33 |
    34 | 39 | 51 |
    52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/icons/qwik.tsx: -------------------------------------------------------------------------------- 1 | export const QwikLogo = () => ( 2 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /src/components/router-head/router-head.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from "@builder.io/qwik"; 2 | import { useDocumentHead, useLocation } from "@builder.io/qwik-city"; 3 | 4 | /** The RouterHead component is placed inside of the document `` element. */ 5 | export const RouterHead = component$(() => { 6 | const head = useDocumentHead(); 7 | const loc = useLocation(); 8 | 9 | return ( 10 | <> 11 | {head.title} 12 | 13 | 14 | 15 | 16 | 17 | {head.meta.map((m) => ( 18 | 19 | ))} 20 | 21 | {head.links.map((l) => ( 22 | 23 | ))} 24 | 25 | {head.styles.map((s) => ( 26 |