├── .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 |
40 |
41 |
42 |
43 |
44 | -
45 | |
46 |
47 | -
48 | {$localize`Blog`}
49 |
50 |
51 |
52 | );
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/icons/qwik.tsx:
--------------------------------------------------------------------------------
1 | export const QwikLogo = () => (
2 |
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 |
27 | ))}
28 | >
29 | );
30 | });
31 |
--------------------------------------------------------------------------------
/src/entry.dev.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * WHAT IS THIS FILE?
3 | *
4 | * Development entry point using only client-side modules:
5 | * - Do not use this mode in production!
6 | * - No SSR
7 | * - No portion of the application is pre-rendered on the server.
8 | * - All of the application is running eagerly in the browser.
9 | * - More code is transferred to the browser than in SSR mode.
10 | * - Optimizer/Serialization/Deserialization code is not exercised!
11 | */
12 | import { render, type RenderOptions } from "@builder.io/qwik";
13 | import Root from "./root";
14 |
15 | export default function (opts: RenderOptions) {
16 | return render(document, , opts);
17 | }
18 |
--------------------------------------------------------------------------------
/src/entry.express.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * WHAT IS THIS FILE?
3 | *
4 | * It's the entry point for the Express HTTP server when building for production.
5 | *
6 | * Learn more about Node.js server integrations here:
7 | * - https://qwik.builder.io/docs/deployments/node/
8 | *
9 | */
10 | import {
11 | createQwikCity,
12 | type PlatformNode,
13 | } from "@builder.io/qwik-city/middleware/node";
14 | import qwikCityPlan from "@qwik-city-plan";
15 | import { manifest } from "@qwik-client-manifest";
16 | import render from "./entry.ssr";
17 | import express from "express";
18 | import { fileURLToPath } from "node:url";
19 | import { join } from "node:path";
20 | import compression from "compression";
21 |
22 | declare global {
23 | interface QwikCityPlatform extends PlatformNode {}
24 | }
25 |
26 | // Directories where the static assets are located
27 | const distDir = join(fileURLToPath(import.meta.url), "..", "..", "dist");
28 | const buildDir = join(distDir, "build");
29 |
30 | // Allow for dynamic port
31 | const PORT = process.env.PORT ?? 3000;
32 |
33 | // Create the Qwik City Node middleware
34 | const { router, notFound } = createQwikCity({
35 | render,
36 | qwikCityPlan,
37 | manifest,
38 | // getOrigin(req) {
39 | // // If deploying under a proxy, you may need to build the origin from the request headers
40 | // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
41 | // const protocol = req.headers["x-forwarded-proto"] ?? "http";
42 | // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
43 | // const host = req.headers["x-forwarded-host"] ?? req.headers.host;
44 | // return `${protocol}://${host}`;
45 | // }
46 | });
47 |
48 | // Create the express server
49 | // https://expressjs.com/
50 | const app = express();
51 |
52 | // Enable gzip compression
53 | app.use(compression());
54 |
55 | // Static asset handlers
56 | // https://expressjs.com/en/starter/static-files.html
57 | app.use(`/build`, express.static(buildDir, { immutable: true, maxAge: "1y" }));
58 | app.use(express.static(distDir, { redirect: false }));
59 |
60 | // Use Qwik City's page and endpoint request handler
61 | app.use(router);
62 |
63 | // Use Qwik City's 404 handler
64 | app.use(notFound);
65 |
66 | // Start the express server
67 | app.listen(PORT, () => {
68 | /* eslint-disable */
69 | console.log(`Server started: http://localhost:${PORT}/`);
70 | });
71 |
--------------------------------------------------------------------------------
/src/entry.preview.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * WHAT IS THIS FILE?
3 | *
4 | * It's the bundle entry point for `npm run preview`.
5 | * That is, serving your app built in production mode.
6 | *
7 | * Feel free to modify this file, but don't remove it!
8 | *
9 | * Learn more about Vite's preview command:
10 | * - https://vitejs.dev/config/preview-options.html#preview-options
11 | *
12 | */
13 | import { createQwikCity } from "@builder.io/qwik-city/middleware/node";
14 | import qwikCityPlan from "@qwik-city-plan";
15 | import render from "./entry.ssr";
16 |
17 | /** The default export is the QwikCity adapter used by Vite preview. */
18 | export default createQwikCity({ render, qwikCityPlan });
19 |
--------------------------------------------------------------------------------
/src/entry.ssr.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * WHAT IS THIS FILE?
3 | *
4 | * SSR entry point, in all cases the application is render outside the browser, this
5 | * entry point will be the common one.
6 | *
7 | * - Server (express, cloudflare...)
8 | * - npm run start
9 | * - npm run preview
10 | * - npm run build
11 | *
12 | */
13 | import { renderToStream, RenderToStreamOptions } from "@builder.io/qwik/server";
14 | import { manifest } from "@qwik-client-manifest";
15 | import Root from "./root";
16 | import { extractBase } from "./i18n";
17 |
18 | export default function (opts: RenderToStreamOptions) {
19 | return renderToStream(, {
20 | manifest,
21 | ...opts,
22 | base: extractBase, // determine the base URL for the client code
23 | // Use container attributes to set attributes on the html tag.
24 | containerAttributes: {
25 | lang: opts.serverData!.locale,
26 | ...opts.containerAttributes,
27 | },
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | /**
2 | * WHAT IS THIS FILE?
3 | *
4 | * Globally applied styles. No matter which components are in the page or matching route,
5 | * the styles in here will be applied to the Document, without any sort of CSS scoping.
6 | *
7 | */
8 |
9 | :root {
10 | --qwik-dark-blue: #006ce9;
11 | --qwik-light-blue: #18b6f6;
12 | --qwik-light-purple: #ac7ff4;
13 | --qwik-dark-purple: #713fc2;
14 | }
15 |
16 | body {
17 | background-color: #fafafa;
18 | font-family:
19 | "Poppins",
20 | ui-sans-serif,
21 | system-ui,
22 | -apple-system,
23 | BlinkMacSystemFont,
24 | sans-serif;
25 | padding: 20px 20px 40px 20px;
26 | }
27 |
28 | main {
29 | max-width: 760px;
30 | margin: 0 auto;
31 | background-color: white;
32 | border-radius: 5px;
33 | box-shadow: 0px 0px 130px -50px var(--qwik-light-purple);
34 | overflow: hidden;
35 | }
36 |
37 | h1,
38 | h2 {
39 | margin: 0 0 5px 0;
40 | }
41 |
42 | .lightning {
43 | filter: hue-rotate(180deg);
44 | }
45 |
46 | section {
47 | padding: 20px;
48 | border-bottom: 10px solid var(--qwik-dark-blue);
49 | }
50 |
51 | ul {
52 | list-style-type: square;
53 | margin: 5px 0 30px 0;
54 | padding-left: 25px;
55 | }
56 |
57 | li {
58 | padding: 5px 0;
59 | }
60 |
61 | li::marker {
62 | color: var(--qwik-light-blue);
63 | }
64 |
65 | a,
66 | a:visited {
67 | color: var(--qwik-dark-blue);
68 | }
69 |
70 | a:hover {
71 | text-decoration: none;
72 | }
73 |
74 | table.commands {
75 | margin: 0 0 30px 0;
76 | }
77 |
78 | .commands td {
79 | padding: 5px;
80 | }
81 |
82 | .commands td:first-child {
83 | white-space: nowrap;
84 | padding-right: 20px;
85 | }
86 |
87 | code {
88 | font-family:
89 | Menlo,
90 | Monaco,
91 | Courier New,
92 | monospace;
93 | font-size: 0.9em;
94 | background-color: rgb(224, 224, 224);
95 | padding: 2px 4px;
96 | border-radius: 3px;
97 | border-bottom: 2px solid #bfbfbf;
98 | }
99 |
100 | footer {
101 | padding: 15px;
102 | text-align: center;
103 | font-size: 0.8em;
104 | }
105 |
106 | footer a {
107 | text-decoration: none;
108 | }
109 |
110 | footer a:hover {
111 | text-decoration: underline;
112 | }
113 |
114 | a.mindblow {
115 | margin: 0 auto;
116 | display: block;
117 | background: var(--qwik-light-purple);
118 | padding: 10px 20px;
119 | border-radius: 10px;
120 | border: 0;
121 | color: white;
122 | text-decoration: none;
123 | font-size: 20px;
124 | width: fit-content;
125 | border-bottom: 4px solid black;
126 | cursor:
127 | url("data:image/svg+xml;utf8,")
128 | 16 0,
129 | auto; /*!emojicursor.app*/
130 | }
131 |
132 | a.mindblow:hover {
133 | border-bottom-width: 0px;
134 | margin-bottom: 4px;
135 | transform: translateY(4px);
136 | }
137 |
--------------------------------------------------------------------------------
/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import "@angular/localize/init";
2 | import { loadTranslations } from "@angular/localize";
3 | import { getLocale, withLocale, useOnDocument, $ } from "@builder.io/qwik";
4 | import type { RenderOptions } from "@builder.io/qwik/server";
5 |
6 | // You must declare all your locales here
7 | import EN from "./locale/message.en.json";
8 | import SK from "./locale/message.sk.json";
9 | import FR from "./locale/message.fr.json";
10 | import SP from "./locale/message.sp.json";
11 |
12 | // Make sure it's obvious when the default locale was selected
13 | const defaultLocale = "sk";
14 |
15 | /**
16 | * Function used to load all translations variants.
17 | */
18 | export function initTranslations() {
19 | console.log(" ➜ Loading translations...");
20 | [SK, EN, FR, SP].forEach(({ translations, locale }) => {
21 | // withLocale sets the locale for the duration of the callback
22 | withLocale(locale, () => loadTranslations(translations));
23 | });
24 | }
25 |
26 | /**
27 | * This file is left for the developer to customize to get the behavior they want for localization.
28 | */
29 |
30 | /// Declare location where extra types will be stored.
31 | const $localizeFn = $localize as any as {
32 | TRANSLATIONS: Record;
33 | TRANSLATION_BY_LOCALE: Map>;
34 | };
35 |
36 | /**
37 | * This solution uses the `@angular/localize` package for translations, however out of the box
38 | * `$localize` works with a single translation only. This code adds support for multiple locales
39 | * concurrently. It does this by intercepting the `TRANSLATIONS` property read and returning
40 | * appropriate translation based on the current locale.
41 | */
42 | if (!$localizeFn.TRANSLATION_BY_LOCALE) {
43 | $localizeFn.TRANSLATION_BY_LOCALE = new Map([["", {}]]);
44 | Object.defineProperty($localizeFn, "TRANSLATIONS", {
45 | get: function () {
46 | const locale = getLocale();
47 | let translations = this.TRANSLATION_BY_LOCALE.get(locale);
48 | if (!translations) {
49 | this.TRANSLATION_BY_LOCALE.set(locale, (translations = {}));
50 | }
51 | return translations;
52 | },
53 | });
54 | }
55 |
56 | const validateLocale = (locale: string) => {
57 | if ($localizeFn.TRANSLATION_BY_LOCALE.has(locale)) return locale;
58 | const match = /^([^-;]+)[-;]/.exec(locale);
59 | return (
60 | (match && $localizeFn.TRANSLATION_BY_LOCALE.has(match[1]) && match[1]) ||
61 | undefined
62 | );
63 | };
64 |
65 | /**
66 | * Function used to examine the request and determine the locale to use.
67 | *
68 | * in this implementation, we accept a `?locale=xx` parameter to override
69 | * the auto-detected locale requested by the browser.
70 | *
71 | * This function is meant to be used with `RenderOptions.locale` property.
72 | * It must always return a valid locale so that prod clients will always get de-$localize-d js
73 | *
74 | * @returns The locale to use, which will be stored in the render context.
75 | */
76 | export function extractLang(request: Request, url: URL): string {
77 | // This is not really needed because we handle /locale, but it's here as an example
78 | let locale = url.searchParams.get("locale") || undefined;
79 | if (locale) {
80 | // note that we mutate the URL here, this will update the search property
81 | url.searchParams.delete("locale");
82 | locale = validateLocale(locale);
83 | if (locale) return locale;
84 | }
85 | // Parse the browser accept-language header
86 | const locales = request.headers.get("accept-language")?.split(",");
87 | if (locales)
88 | for (const entry of locales) {
89 | locale = validateLocale(entry);
90 | if (locale) return locale;
91 | }
92 |
93 | return defaultLocale;
94 | }
95 |
96 | /**
97 | * Function used to determine the base URL to use for loading the chunks in the browser.
98 | *
99 | * The function returns `/build` in dev mode or `/build/` in prod mode.
100 | *
101 | * This function is meant to be used with `RenderOptions.base` property
102 | *
103 | * @returns The base URL to use for loading the chunks in the browser.
104 | */
105 | export function extractBase({ serverData }: RenderOptions): string {
106 | if (import.meta.env.DEV) {
107 | return "/build";
108 | } else {
109 | return "/build/" + serverData!.locale;
110 | }
111 | }
112 |
113 | export function useI18n() {
114 | if (import.meta.env.DEV) {
115 | // During development only, load all translations in memory when the app starts on the client.
116 | // eslint-disable-next-line qwik/use-method-usage
117 | useOnDocument("qinit", $(initTranslations));
118 | }
119 | }
120 |
121 | // We always need the translations on the server
122 | if (import.meta.env.SSR) initTranslations();
123 |
--------------------------------------------------------------------------------
/src/locale/message.en.json:
--------------------------------------------------------------------------------
1 | {
2 | "locale": "en",
3 | "translations": {
4 | "7751010942038334793": "Blog",
5 | "7683568525587144503": "/en/blog",
6 | "8399228546444251220": "Counter Example",
7 | "6030848919533485936": "Hello and welcome to the blog",
8 | "2023484548631819319": "Hello world",
9 | "3957345415493603866": "/en/blog/{$PH}/",
10 | "4608414764123111425": "count: {$PH}",
11 | "2954233255021387859": "increment"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/locale/message.fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "locale": "fr",
3 | "translations": {
4 | "4608414764123111425": "compter: {$PH}",
5 | "8399228546444251220": "Exemple de compteur",
6 | "2954233255021387859": "incrément",
7 | "7683568525587144503": "/fr/blog",
8 | "3957345415493603866": "/fr/blog/{$PH}/",
9 | "7751010942038334793": "Blogue",
10 | "6030848919533485936": "Bonjour et bienvue sur mon blogue!",
11 | "2023484548631819319": "Hello world"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/locale/message.sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "locale": "sk",
3 | "translations": {
4 | "8399228546444251220": "Počítadlovy Príklad",
5 | "4608414764123111425": "počítadlo: {$PH}",
6 | "2954233255021387859": "pridať",
7 | "7751010942038334793": "Blogu",
8 | "7683568525587144503": "/sk/blog",
9 | "6030848919533485936": "Ahoj a vitajte na blogu",
10 | "2023484548631819319": "Ahoj svet!",
11 | "3957345415493603866": "/sk/blog/{$PH}/"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/locale/message.sp.json:
--------------------------------------------------------------------------------
1 | {
2 | "locale": "sp",
3 | "translations": {
4 | "4608414764123111425": "contar: {$PH}",
5 | "8399228546444251220": "Ejemplo de contador",
6 | "2954233255021387859": "incremento",
7 | "7751010942038334793": "Blog",
8 | "7683568525587144503": "/es/blog",
9 | "6030848919533485936": "Hola y bienvenido al blog",
10 | "2023484548631819319": "Hola mundo",
11 | "3957345415493603866": "/es/blog/{$PH}/"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/root.tsx:
--------------------------------------------------------------------------------
1 | import { component$ } from "@builder.io/qwik";
2 | import {
3 | QwikCityProvider,
4 | RouterOutlet,
5 | ServiceWorkerRegister,
6 | } from "@builder.io/qwik-city";
7 | import { RouterHead } from "./components/router-head/router-head";
8 | import "./global.css";
9 | import { useI18n } from "./i18n";
10 |
11 | export default component$(() => {
12 | /**
13 | * The root of a QwikCity site always start with the
14 | * component,
15 | * immediately followed by the document's and .
16 | *
17 | * Don't remove the `` and `` elements.
18 | */
19 |
20 | useI18n();
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | });
35 |
--------------------------------------------------------------------------------
/src/routes/[...locale]/blog/[id]/index.tsx:
--------------------------------------------------------------------------------
1 | import { component$ } from "@builder.io/qwik";
2 | import { useLocation } from "@builder.io/qwik-city";
3 |
4 | export default component$(() => {
5 | const location = useLocation();
6 | return Pretend this is the blog text for "{location.params.id}".
;
7 | });
8 |
--------------------------------------------------------------------------------
/src/routes/[...locale]/blog/index.tsx:
--------------------------------------------------------------------------------
1 | import { component$ } from "@builder.io/qwik";
2 | import { Link } from "@builder.io/qwik-city";
3 |
4 | export default component$(() => (
5 |
6 |
{$localize`Blog welcome text`}
7 |
{$localize`Hello world`}
10 |
11 | ));
12 |
--------------------------------------------------------------------------------
/src/routes/[...locale]/index.tsx:
--------------------------------------------------------------------------------
1 | import { component$, useStore } from "@builder.io/qwik";
2 | import type { DocumentHead } from "@builder.io/qwik-city";
3 |
4 | export default component$(() => {
5 | const state = useStore({
6 | count: 0,
7 | });
8 | return (
9 | <>
10 | {$localize`Counter Example`}
11 | {$localize`count: ${state.count}`}
12 |
15 | >
16 | );
17 | });
18 |
19 | export const head: DocumentHead = () => {
20 | return {
21 | title: $localize`Counter Example`,
22 | meta: [
23 | {
24 | name: "description",
25 | content: "Qwik site description",
26 | },
27 | ],
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/routes/[...locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { component$, Slot } from "@builder.io/qwik";
2 | import Header from "~/components/header/header";
3 | import type { RequestHandler } from "@builder.io/qwik-city";
4 | import { extractLang } from "~/i18n";
5 |
6 | const locales = new Set(["en", "fr", "sk", "sp"]);
7 |
8 | export const onGet: RequestHandler = async ({
9 | request,
10 | url,
11 | redirect,
12 | pathname,
13 | params,
14 | locale,
15 | cacheControl,
16 | }) => {
17 | if (locales.has(params.locale)) {
18 | // Set the locale for this request
19 | // TODO be case-insensitive
20 | locale(params.locale);
21 | } else {
22 | // Redirect to the correct locale
23 | const guessedLocale = extractLang(request, url);
24 | let path;
25 | if (
26 | params.locale === "__" ||
27 | /^[a-z][a-z](-[a-z][a-z])?/i.test(params.locale)
28 | ) {
29 | // invalid locale
30 | // TODO a better way to replace the locale parameter that supports a base path
31 | path = "/" + pathname.split("/").slice(2).join("/");
32 | } else {
33 | // no locale
34 | path = pathname;
35 | }
36 | throw redirect(301, `/${guessedLocale}${path}${url.search}`);
37 | }
38 |
39 | // Control caching for this request for best performance and to reduce hosting costs:
40 | // https://qwik.builder.io/docs/caching/
41 | cacheControl({
42 | // Always serve a cached response by default, up to a week stale
43 | staleWhileRevalidate: 60 * 60 * 24 * 7,
44 | // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
45 | maxAge: 5,
46 | });
47 | };
48 |
49 | export default component$(() => {
50 | return (
51 | <>
52 |
53 |
54 |
57 |
58 |
63 | >
64 | );
65 | });
66 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from "@builder.io/qwik-city";
2 | import { extractLang } from "~/i18n";
3 |
4 | export const onGet: RequestHandler = async ({ request, redirect, url }) => {
5 | const guessedLocale = extractLang(request, url);
6 | console.log(` ➜ GET / - Redirecting to /${guessedLocale}...`);
7 | throw redirect(301, `/${guessedLocale}/${url.search}`);
8 | };
9 |
--------------------------------------------------------------------------------
/src/routes/service-worker.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * WHAT IS THIS FILE?
3 | *
4 | * The service-worker.ts file is used to have state of the art prefetching.
5 | * https://qwik.builder.io/qwikcity/prefetching/overview/
6 | *
7 | * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline.
8 | * You can also use this file to add more functionality that runs in the service worker.
9 | */
10 | import { setupServiceWorker } from "@builder.io/qwik-city/service-worker";
11 |
12 | setupServiceWorker();
13 |
14 | addEventListener("install", () => self.skipWaiting());
15 |
16 | addEventListener("activate", () => self.clients.claim());
17 |
18 | declare const self: ServiceWorkerGlobalScope;
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "target": "ES2017",
5 | "module": "ES2020",
6 | "lib": ["es2020", "DOM", "WebWorker"],
7 | "jsx": "react-jsx",
8 | "jsxImportSource": "@builder.io/qwik",
9 | "strict": true,
10 | "resolveJsonModule": true,
11 | "moduleResolution": "node",
12 | "esModuleInterop": true,
13 | "skipLibCheck": true,
14 | "incremental": true,
15 | "isolatedModules": true,
16 | "types": ["node", "vite/client"],
17 | "paths": {
18 | "~/*": ["./src/*"]
19 | }
20 | },
21 | "include": ["src"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { qwikVite } from "@builder.io/qwik/optimizer";
3 | import { qwikCity } from "@builder.io/qwik-city/vite";
4 | import tsconfigPaths from "vite-tsconfig-paths";
5 |
6 | export default defineConfig(() => {
7 | return {
8 | plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
9 | };
10 | });
11 |
--------------------------------------------------------------------------------