@nuxtjp/localized-site 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/LICENSE-ASSETS +35 -0
- package/LICENSE-PREVIOUS +21 -0
- package/NOTICE +4 -0
- package/README.ja.md +66 -0
- package/README.md +65 -2
- package/bin/nuxtjp-localized-site.mjs +7 -0
- package/dist/module.d.mts +62 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +74 -0
- package/dist/runtime/app/assets/localized-site.css +1 -0
- package/dist/runtime/app/components/LocalizedDocument.d.vue.ts +7 -0
- package/dist/runtime/app/components/LocalizedDocument.vue +23 -0
- package/dist/runtime/app/components/LocalizedDocument.vue.d.ts +7 -0
- package/dist/runtime/app/components/LocalizedLanding.d.vue.ts +3 -0
- package/dist/runtime/app/components/LocalizedLanding.vue +25 -0
- package/dist/runtime/app/components/LocalizedLanding.vue.d.ts +3 -0
- package/dist/runtime/app/components/LocalizedLanguageSwitcher.d.vue.ts +3 -0
- package/dist/runtime/app/components/LocalizedLanguageSwitcher.vue +21 -0
- package/dist/runtime/app/components/LocalizedLanguageSwitcher.vue.d.ts +3 -0
- package/dist/runtime/app/components/PublicMaintenanceNotice.d.vue.ts +3 -0
- package/dist/runtime/app/components/PublicMaintenanceNotice.vue +29 -0
- package/dist/runtime/app/components/PublicMaintenanceNotice.vue.d.ts +3 -0
- package/dist/runtime/app/composables/useLocalizedMessages.d.ts +10 -0
- package/dist/runtime/app/composables/useLocalizedMessages.js +14 -0
- package/dist/runtime/app/composables/useLocalizedSite.d.ts +9 -0
- package/dist/runtime/app/composables/useLocalizedSite.js +25 -0
- package/dist/runtime/app/layouts/localized-site.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/localized-site.vue +37 -0
- package/dist/runtime/app/layouts/localized-site.vue.d.ts +13 -0
- package/dist/runtime/app/pages/LocalizedContentPage.d.vue.ts +3 -0
- package/dist/runtime/app/pages/LocalizedContentPage.vue +13 -0
- package/dist/runtime/app/pages/LocalizedContentPage.vue.d.ts +3 -0
- package/dist/runtime/app/plugins/localized-head.d.ts +2 -0
- package/dist/runtime/app/plugins/localized-head.js +7 -0
- package/dist/runtime/core/index.d.ts +8 -0
- package/dist/runtime/core/index.js +14 -0
- package/dist/runtime/core/maintenance.d.ts +25 -0
- package/dist/runtime/core/maintenance.js +51 -0
- package/dist/runtime/core/messages.d.ts +13 -0
- package/dist/runtime/core/messages.js +11 -0
- package/dist/runtime/core/path.d.ts +6 -0
- package/dist/runtime/core/path.js +43 -0
- package/dist/runtime/core/seo.d.ts +20 -0
- package/dist/runtime/core/seo.js +52 -0
- package/dist/runtime/shared/nuxt-imports.d.ts +18 -0
- package/dist/types.d.mts +3 -0
- package/examples/content/site.config.json +90 -0
- package/package.json +75 -3
- package/schemas/localized-site-v1.schema.json +139 -0
- package/schemas/public-maintenance-state-v1.schema.json +50 -0
- package/validator/content/cli.d.ts +5 -0
- package/validator/content/cli.js +31 -0
- package/validator/content/errors.d.ts +4 -0
- package/validator/content/errors.js +8 -0
- package/validator/content/index.d.ts +4 -0
- package/validator/content/index.js +3 -0
- package/validator/content/load.d.ts +2 -0
- package/validator/content/load.js +25 -0
- package/validator/content/validation/completeness.d.ts +2 -0
- package/validator/content/validation/completeness.js +21 -0
- package/validator/content/validation/locale-content.d.ts +2 -0
- package/validator/content/validation/locale-content.js +86 -0
- package/validator/content/validation/primitives.d.ts +9 -0
- package/validator/content/validation/primitives.js +48 -0
- package/validator/content/validation/site-config.d.ts +3 -0
- package/validator/content/validation/site-config.js +88 -0
- package/validator/contracts/types.d.ts +53 -0
- package/validator/contracts/types.js +3 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useLocalizedSite } from "../composables/useLocalizedSite";
|
|
3
|
+
const site = useLocalizedSite();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<nav
|
|
8
|
+
class="nls-language-switcher"
|
|
9
|
+
:aria-label="site.locale.value === 'ja' ? '\u8A00\u8A9E' : 'Language'"
|
|
10
|
+
>
|
|
11
|
+
<NuxtLink
|
|
12
|
+
v-for="locale in site.supportedLocales"
|
|
13
|
+
:key="locale"
|
|
14
|
+
:to="site.localePath(locale)"
|
|
15
|
+
:lang="locale"
|
|
16
|
+
:aria-current="locale === site.locale.value ? 'page' : void 0"
|
|
17
|
+
>
|
|
18
|
+
{{ locale === "ja" ? "\u65E5\u672C\u8A9E" : "English" }}
|
|
19
|
+
</NuxtLink>
|
|
20
|
+
</nav>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, ref } from "vue";
|
|
3
|
+
import { maintenanceForService } from "../../core/maintenance";
|
|
4
|
+
import { useLocalizedSite } from "../composables/useLocalizedSite";
|
|
5
|
+
const site = useLocalizedSite();
|
|
6
|
+
const notice = ref(null);
|
|
7
|
+
onMounted(async () => {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch("/maintenance.json", {
|
|
10
|
+
credentials: "same-origin",
|
|
11
|
+
signal: AbortSignal.timeout(2e3)
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) throw new Error("Maintenance state is unavailable");
|
|
14
|
+
const source = await response.text();
|
|
15
|
+
if (source.length > 65536) throw new Error("Maintenance state is too large");
|
|
16
|
+
const document = JSON.parse(source);
|
|
17
|
+
notice.value = maintenanceForService(document, site.config.serviceId);
|
|
18
|
+
} catch {
|
|
19
|
+
notice.value = null;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<aside v-if="notice" class="nls-maintenance" role="status">
|
|
26
|
+
<strong>{{ site.locale.value === "ja" ? "\u30E1\u30F3\u30C6\u30CA\u30F3\u30B9\u4E2D" : "Maintenance" }}</strong>
|
|
27
|
+
<span>{{ notice[site.locale.value] }}</span>
|
|
28
|
+
</aside>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LocalizedMessages } from '../../core/messages.js';
|
|
2
|
+
export declare function useLocalizedMessages<Key extends string>(messages: LocalizedMessages<Key>): {
|
|
3
|
+
t: (key: Key) => string;
|
|
4
|
+
config: any;
|
|
5
|
+
content: import("vue").ComputedRef<any>;
|
|
6
|
+
locale: import("vue").ComputedRef<"ja" | "en">;
|
|
7
|
+
localePath: (localeValue: import("../../core/index.js").SupportedLocale, path?: any) => string;
|
|
8
|
+
pageId: import("vue").ComputedRef<import("../../../contracts/types.js").ContentPageId | undefined>;
|
|
9
|
+
supportedLocales: any;
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { translatedMessage } from "../../core/messages.js";
|
|
2
|
+
import { useLocalizedSite } from "./useLocalizedSite.js";
|
|
3
|
+
export function useLocalizedMessages(messages) {
|
|
4
|
+
const site = useLocalizedSite();
|
|
5
|
+
function t(key) {
|
|
6
|
+
return translatedMessage(
|
|
7
|
+
messages,
|
|
8
|
+
key,
|
|
9
|
+
site.locale.value,
|
|
10
|
+
site.config
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return { ...site, t };
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SupportedLocale } from '../../../contracts/types.js';
|
|
2
|
+
export declare function useLocalizedSite(): {
|
|
3
|
+
config: any;
|
|
4
|
+
content: import("vue").ComputedRef<any>;
|
|
5
|
+
locale: import("vue").ComputedRef<"ja" | "en">;
|
|
6
|
+
localePath: (localeValue: SupportedLocale, path?: any) => string;
|
|
7
|
+
pageId: import("vue").ComputedRef<import("../../../contracts/types.js").ContentPageId | undefined>;
|
|
8
|
+
supportedLocales: any;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRoute, useRuntimeConfig } from "#imports";
|
|
3
|
+
import {
|
|
4
|
+
localeFromPath,
|
|
5
|
+
localePath as buildLocalePath,
|
|
6
|
+
pageIdFromPath
|
|
7
|
+
} from "../../core/path.js";
|
|
8
|
+
export function useLocalizedSite() {
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
const config = useRuntimeConfig().public.nuxtJpLocalizedSite;
|
|
11
|
+
const locale = computed(() => localeFromPath(route.path, config.defaultLocale));
|
|
12
|
+
const content = computed(() => config.content[locale.value]);
|
|
13
|
+
const pageId = computed(() => pageIdFromPath(route.path));
|
|
14
|
+
function localePath(localeValue, path = route.fullPath) {
|
|
15
|
+
return buildLocalePath(path, localeValue, config);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
config,
|
|
19
|
+
content,
|
|
20
|
+
locale,
|
|
21
|
+
localePath,
|
|
22
|
+
pageId,
|
|
23
|
+
supportedLocales: config.locales
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare var __VLS_29: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
default?: (props: typeof __VLS_29) => any;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
9
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
10
|
+
new (): {
|
|
11
|
+
$slots: S;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useLocalizedSite } from "#imports";
|
|
3
|
+
const site = useLocalizedSite();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="nls-shell">
|
|
8
|
+
<header class="nls-header">
|
|
9
|
+
<NuxtLink :to="site.localePath(site.locale.value, '/')">
|
|
10
|
+
{{ site.content.value.brand }}
|
|
11
|
+
</NuxtLink>
|
|
12
|
+
<nav :aria-label="site.locale.value === 'ja' ? '\u4E3B\u8981' : 'Primary'">
|
|
13
|
+
<NuxtLink :to="site.localePath(site.locale.value, '/')">
|
|
14
|
+
{{ site.locale.value === "ja" ? "\u30DB\u30FC\u30E0" : "Home" }}
|
|
15
|
+
</NuxtLink>
|
|
16
|
+
<NuxtLink :to="site.localePath(site.locale.value, '/docs')">
|
|
17
|
+
{{ site.content.value.docs.overview.title }}
|
|
18
|
+
</NuxtLink>
|
|
19
|
+
</nav>
|
|
20
|
+
<NuxtJpLocalizedLanguageSwitcher />
|
|
21
|
+
</header>
|
|
22
|
+
<NuxtJpPublicMaintenanceNotice />
|
|
23
|
+
<main id="main-content" class="nls-main">
|
|
24
|
+
<slot />
|
|
25
|
+
</main>
|
|
26
|
+
<footer class="nls-footer">
|
|
27
|
+
<section>
|
|
28
|
+
<strong>{{ site.content.value.privacy.label }}</strong>
|
|
29
|
+
<p>{{ site.content.value.privacy.summary }}</p>
|
|
30
|
+
</section>
|
|
31
|
+
<section>
|
|
32
|
+
<strong>{{ site.content.value.contact.label }}</strong>
|
|
33
|
+
<p>{{ site.content.value.contact.summary }}</p>
|
|
34
|
+
</section>
|
|
35
|
+
</footer>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare var __VLS_29: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
default?: (props: typeof __VLS_29) => any;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
9
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
10
|
+
new (): {
|
|
11
|
+
$slots: S;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import LocalizedDocument from "../components/LocalizedDocument.vue";
|
|
4
|
+
import LocalizedLanding from "../components/LocalizedLanding.vue";
|
|
5
|
+
import { useLocalizedSite } from "../composables/useLocalizedSite";
|
|
6
|
+
const site = useLocalizedSite();
|
|
7
|
+
const documentId = computed(() => site.pageId.value && site.pageId.value !== "landing" ? site.pageId.value : void 0);
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<LocalizedLanding v-if="!documentId" />
|
|
12
|
+
<LocalizedDocument v-else :document-id="documentId" />
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useHead, useRoute, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { localizedHead } from "../../core/seo.js";
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
const config = useRuntimeConfig().public.nuxtJpLocalizedSite;
|
|
6
|
+
useHead(() => localizedHead(route.path, config));
|
|
7
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { defineLocalizedMessages, translatedMessage } from './messages.js';
|
|
2
|
+
export type { LocalizedMessages, MessageDictionary } from './messages.js';
|
|
3
|
+
export { localeFromPath, localePath, pageIdFromPath, siteRoutes, unlocalizedPath } from './path.js';
|
|
4
|
+
export { localizedHead } from './seo.js';
|
|
5
|
+
export type { HeadLink, HeadMeta, LocalizedHead } from './seo.js';
|
|
6
|
+
export { MAINTENANCE_SCHEMA, maintenanceForService, parseMaintenanceDocument } from './maintenance.js';
|
|
7
|
+
export type { MaintenanceMessage, MaintenanceState, MaintenanceTarget, PublicMaintenanceDocument } from './maintenance.js';
|
|
8
|
+
export type { ResolvedSiteConfig, SiteConfig, SupportedLocale } from '../../contracts/types.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { defineLocalizedMessages, translatedMessage } from "./messages.js";
|
|
2
|
+
export {
|
|
3
|
+
localeFromPath,
|
|
4
|
+
localePath,
|
|
5
|
+
pageIdFromPath,
|
|
6
|
+
siteRoutes,
|
|
7
|
+
unlocalizedPath
|
|
8
|
+
} from "./path.js";
|
|
9
|
+
export { localizedHead } from "./seo.js";
|
|
10
|
+
export {
|
|
11
|
+
MAINTENANCE_SCHEMA,
|
|
12
|
+
maintenanceForService,
|
|
13
|
+
parseMaintenanceDocument
|
|
14
|
+
} from "./maintenance.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MAINTENANCE_SCHEMA = "nuxtjp://public/maintenance-state/v1";
|
|
2
|
+
export type MaintenanceState = 'operational' | 'maintenance';
|
|
3
|
+
export interface MaintenanceMessage {
|
|
4
|
+
ja: string;
|
|
5
|
+
en: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MaintenanceTarget {
|
|
8
|
+
serviceId: string;
|
|
9
|
+
state: MaintenanceState;
|
|
10
|
+
message: MaintenanceMessage;
|
|
11
|
+
expiresAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PublicMaintenanceDocument {
|
|
14
|
+
schema: typeof MAINTENANCE_SCHEMA;
|
|
15
|
+
generatedAt: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
global: {
|
|
18
|
+
state: MaintenanceState;
|
|
19
|
+
message: MaintenanceMessage;
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
};
|
|
22
|
+
services: MaintenanceTarget[];
|
|
23
|
+
}
|
|
24
|
+
export declare function parseMaintenanceDocument(value: unknown): PublicMaintenanceDocument | null;
|
|
25
|
+
export declare function maintenanceForService(value: unknown, serviceId: string, now?: number): MaintenanceMessage | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const MAINTENANCE_SCHEMA = "nuxtjp://public/maintenance-state/v1";
|
|
2
|
+
function record(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function message(value) {
|
|
6
|
+
if (!record(value) || Object.keys(value).sort().join(",") !== "en,ja") return false;
|
|
7
|
+
return ["ja", "en"].every((key) => {
|
|
8
|
+
const text = value[key];
|
|
9
|
+
return typeof text === "string" && text.trim().length > 0 && text.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(text);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function state(value) {
|
|
13
|
+
return value === "operational" || value === "maintenance";
|
|
14
|
+
}
|
|
15
|
+
function target(value) {
|
|
16
|
+
if (!record(value)) return false;
|
|
17
|
+
if (Object.keys(value).sort().join(",") !== "expiresAt,message,serviceId,state") return false;
|
|
18
|
+
return typeof value.serviceId === "string" && /^[a-z][a-z0-9-]{0,63}$/u.test(value.serviceId) && state(value.state) && message(value.message) && validTimestamp(value.expiresAt);
|
|
19
|
+
}
|
|
20
|
+
export function parseMaintenanceDocument(value) {
|
|
21
|
+
if (!record(value)) return null;
|
|
22
|
+
const keys = Object.keys(value).sort().join(",");
|
|
23
|
+
if (keys !== "expiresAt,generatedAt,global,schema,services") return null;
|
|
24
|
+
if (value.schema !== MAINTENANCE_SCHEMA || !record(value.global)) return null;
|
|
25
|
+
const globalKeys = Object.keys(value.global).sort().join(",");
|
|
26
|
+
if (globalKeys !== "expiresAt,message,state") return null;
|
|
27
|
+
if (!state(value.global.state) || !message(value.global.message) || !validTimestamp(value.global.expiresAt)) return null;
|
|
28
|
+
if (!Array.isArray(value.services) || value.services.length > 128) return null;
|
|
29
|
+
if (!value.services.every(target)) return null;
|
|
30
|
+
const ids = new Set(value.services.map((item) => item.serviceId));
|
|
31
|
+
if (ids.size !== value.services.length) return null;
|
|
32
|
+
if (!validTimestamp(value.generatedAt) || !validTimestamp(value.expiresAt)) return null;
|
|
33
|
+
const generated = Date.parse(value.generatedAt);
|
|
34
|
+
const expires = Date.parse(value.expiresAt);
|
|
35
|
+
const targetExpiries = [
|
|
36
|
+
Date.parse(value.global.expiresAt),
|
|
37
|
+
...value.services.map((item) => Date.parse(item.expiresAt))
|
|
38
|
+
];
|
|
39
|
+
if (generated >= expires || targetExpiries.some((time) => time < generated || time > expires)) return null;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
export function maintenanceForService(value, serviceId, now = Date.now()) {
|
|
43
|
+
const document = parseMaintenanceDocument(value);
|
|
44
|
+
if (!document || Date.parse(document.expiresAt) <= now) return null;
|
|
45
|
+
if (document.global.state === "maintenance" && Date.parse(document.global.expiresAt) > now) return document.global.message;
|
|
46
|
+
const service = document.services.find((item) => item.serviceId === serviceId);
|
|
47
|
+
return service?.state === "maintenance" && Date.parse(service.expiresAt) > now ? service.message : null;
|
|
48
|
+
}
|
|
49
|
+
function validTimestamp(value) {
|
|
50
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u.test(value) && Number.isFinite(Date.parse(value));
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedSiteConfig, SupportedLocale } from '../../contracts/types.js';
|
|
2
|
+
export type MessageDictionary = Record<string, string>;
|
|
3
|
+
export type LocalizedMessages<Key extends string> = {
|
|
4
|
+
readonly ja: Readonly<Record<Key, string>>;
|
|
5
|
+
readonly en: Readonly<Record<Key, string>>;
|
|
6
|
+
};
|
|
7
|
+
type NoExtraKeys<Expected, Actual> = Actual & Record<Exclude<keyof Actual, keyof Expected>, never>;
|
|
8
|
+
export declare function defineLocalizedMessages<const Ja extends MessageDictionary, const En extends Record<keyof Ja, string>>(messages: {
|
|
9
|
+
ja: Ja;
|
|
10
|
+
en: NoExtraKeys<Ja, En>;
|
|
11
|
+
}): LocalizedMessages<Extract<keyof Ja, string>>;
|
|
12
|
+
export declare function translatedMessage<Key extends string>(messages: LocalizedMessages<Key>, key: Key, locale: SupportedLocale, config: Pick<ResolvedSiteConfig, 'defaultLocale' | 'fallbackLocale'>): string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function defineLocalizedMessages(messages) {
|
|
2
|
+
return messages;
|
|
3
|
+
}
|
|
4
|
+
export function translatedMessage(messages, key, locale, config) {
|
|
5
|
+
const order = [locale, config.fallbackLocale, config.defaultLocale].filter((entry, index, values) => values.indexOf(entry) === index);
|
|
6
|
+
for (const candidate of order) {
|
|
7
|
+
const value = messages[candidate][key];
|
|
8
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`localized message is empty in every fallback locale: ${key}`);
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ContentPageId, ResolvedSiteConfig, SupportedLocale } from '../../contracts/types.js';
|
|
2
|
+
export declare function localeFromPath(path: string, defaultLocale: SupportedLocale): SupportedLocale;
|
|
3
|
+
export declare function unlocalizedPath(path: string): string;
|
|
4
|
+
export declare function localePath(path: string, locale: SupportedLocale, config: Pick<ResolvedSiteConfig, 'defaultLocale' | 'prefixDefaultLocale'>): string;
|
|
5
|
+
export declare function pageIdFromPath(path: string): ContentPageId | undefined;
|
|
6
|
+
export declare function siteRoutes(config: ResolvedSiteConfig): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function routeParts(input) {
|
|
2
|
+
if (!input.startsWith("/") || input.startsWith("//") || input.includes("\\")) {
|
|
3
|
+
throw new Error("localized routes must be absolute site paths");
|
|
4
|
+
}
|
|
5
|
+
const suffixIndex = [...["?", "#"]].map((marker) => input.indexOf(marker)).filter((index) => index >= 0).sort((left, right) => left - right)[0];
|
|
6
|
+
const pathname = suffixIndex === void 0 ? input : input.slice(0, suffixIndex);
|
|
7
|
+
const suffix = suffixIndex === void 0 ? "" : input.slice(suffixIndex);
|
|
8
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
9
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
10
|
+
throw new Error("localized routes cannot traverse directories");
|
|
11
|
+
}
|
|
12
|
+
const normalized = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
13
|
+
return { pathname: normalized, suffix };
|
|
14
|
+
}
|
|
15
|
+
export function localeFromPath(path, defaultLocale) {
|
|
16
|
+
const first = routeParts(path).pathname.split("/")[1];
|
|
17
|
+
return first === "ja" || first === "en" ? first : defaultLocale;
|
|
18
|
+
}
|
|
19
|
+
export function unlocalizedPath(path) {
|
|
20
|
+
const { pathname } = routeParts(path);
|
|
21
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
22
|
+
if (segments[0] === "ja" || segments[0] === "en") segments.shift();
|
|
23
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
24
|
+
}
|
|
25
|
+
export function localePath(path, locale, config) {
|
|
26
|
+
const { suffix } = routeParts(path);
|
|
27
|
+
const base = unlocalizedPath(path);
|
|
28
|
+
const needsPrefix = config.prefixDefaultLocale || locale !== config.defaultLocale;
|
|
29
|
+
const localized = needsPrefix ? `/${locale}${base === "/" ? "" : base}` : base;
|
|
30
|
+
return `${localized}${suffix}`;
|
|
31
|
+
}
|
|
32
|
+
export function pageIdFromPath(path) {
|
|
33
|
+
const pagePath = unlocalizedPath(path);
|
|
34
|
+
if (pagePath === "/") return "landing";
|
|
35
|
+
if (pagePath === "/docs") return "overview";
|
|
36
|
+
if (pagePath === "/docs/getting-started") return "gettingStarted";
|
|
37
|
+
if (pagePath === "/docs/security") return "security";
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
export function siteRoutes(config) {
|
|
41
|
+
const paths = ["/", "/docs", "/docs/getting-started", "/docs/security"];
|
|
42
|
+
return config.locales.flatMap((locale) => paths.map((path) => localePath(path, locale, config)));
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedSiteConfig, SupportedLocale } from '../../contracts/types.js';
|
|
2
|
+
export interface HeadLink {
|
|
3
|
+
rel: 'canonical' | 'alternate';
|
|
4
|
+
href: string;
|
|
5
|
+
hreflang?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HeadMeta {
|
|
8
|
+
name?: string;
|
|
9
|
+
property?: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LocalizedHead {
|
|
13
|
+
title: string;
|
|
14
|
+
htmlAttrs: {
|
|
15
|
+
lang: SupportedLocale;
|
|
16
|
+
};
|
|
17
|
+
link: HeadLink[];
|
|
18
|
+
meta: HeadMeta[];
|
|
19
|
+
}
|
|
20
|
+
export declare function localizedHead(path: string, config: ResolvedSiteConfig): LocalizedHead;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { localeFromPath, localePath, pageIdFromPath } from "./path.js";
|
|
2
|
+
function absoluteUrl(path, locale, config) {
|
|
3
|
+
const site = new URL(config.origin);
|
|
4
|
+
const base = site.pathname === "/" ? "" : site.pathname.replace(/\/+$/, "");
|
|
5
|
+
const localized = localePath(path.split(/[?#]/, 1)[0] ?? "/", locale, config);
|
|
6
|
+
site.pathname = `${base}${localized === "/" ? "/" : localized}`;
|
|
7
|
+
site.search = "";
|
|
8
|
+
site.hash = "";
|
|
9
|
+
return site.toString();
|
|
10
|
+
}
|
|
11
|
+
function pageText(path, locale, config) {
|
|
12
|
+
const content = config.content[locale];
|
|
13
|
+
const pageId = pageIdFromPath(path);
|
|
14
|
+
if (!pageId || pageId === "landing") {
|
|
15
|
+
return { title: content.brand, description: content.summary };
|
|
16
|
+
}
|
|
17
|
+
const document = content.docs[pageId];
|
|
18
|
+
return {
|
|
19
|
+
title: `${document.title} | ${content.brand}`,
|
|
20
|
+
description: document.summary
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function localizedHead(path, config) {
|
|
24
|
+
const locale = localeFromPath(path, config.defaultLocale);
|
|
25
|
+
const page = pageText(path, locale, config);
|
|
26
|
+
const canonical = absoluteUrl(path, locale, config);
|
|
27
|
+
const alternates = config.locales.map((alternate) => ({
|
|
28
|
+
rel: "alternate",
|
|
29
|
+
hreflang: alternate,
|
|
30
|
+
href: absoluteUrl(path, alternate, config)
|
|
31
|
+
}));
|
|
32
|
+
return {
|
|
33
|
+
title: page.title,
|
|
34
|
+
htmlAttrs: { lang: locale },
|
|
35
|
+
link: [
|
|
36
|
+
{ rel: "canonical", href: canonical },
|
|
37
|
+
...alternates,
|
|
38
|
+
{
|
|
39
|
+
rel: "alternate",
|
|
40
|
+
hreflang: "x-default",
|
|
41
|
+
href: absoluteUrl(path, config.defaultLocale, config)
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
meta: [
|
|
45
|
+
{ name: "description", content: page.description },
|
|
46
|
+
{ property: "og:locale", content: locale === "ja" ? "ja_JP" : "en_US" },
|
|
47
|
+
{ property: "og:url", content: canonical },
|
|
48
|
+
{ property: "og:title", content: page.title },
|
|
49
|
+
{ property: "og:description", content: page.description }
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare module '#imports' {
|
|
2
|
+
export function defineNuxtPlugin(plugin: () => void): unknown
|
|
3
|
+
export const useLocalizedSite: typeof import(
|
|
4
|
+
'../app/composables/useLocalizedSite'
|
|
5
|
+
).useLocalizedSite
|
|
6
|
+
export function useHead(
|
|
7
|
+
input: () => import('../core/seo').LocalizedHead
|
|
8
|
+
): void
|
|
9
|
+
export function useRoute(): {
|
|
10
|
+
path: string
|
|
11
|
+
fullPath: string
|
|
12
|
+
}
|
|
13
|
+
export function useRuntimeConfig(): {
|
|
14
|
+
public: {
|
|
15
|
+
nuxtJpLocalizedSite: import('../../contracts/types').ResolvedSiteConfig
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "nuxtjp://localized-site/site-config/v1",
|
|
3
|
+
"siteId": "sample-service-site",
|
|
4
|
+
"serviceId": "sample-service",
|
|
5
|
+
"origin": "https://example.test",
|
|
6
|
+
"audienceScope": "japan",
|
|
7
|
+
"defaultLocale": "ja",
|
|
8
|
+
"fallbackLocale": "en",
|
|
9
|
+
"prefixDefaultLocale": false,
|
|
10
|
+
"locales": ["ja", "en"],
|
|
11
|
+
"externalActions": false,
|
|
12
|
+
"content": {
|
|
13
|
+
"ja": {
|
|
14
|
+
"brand": "サンプルサービス",
|
|
15
|
+
"tagline": "ローカル契約から構築する公開サイト",
|
|
16
|
+
"summary": "日本語と英語の内容を同じ構造で管理する例です。",
|
|
17
|
+
"features": [
|
|
18
|
+
{"id": "local-first", "title": "ローカル完結", "summary": "ビルド時に明示したコンテンツだけを読み取ります。"},
|
|
19
|
+
{"id": "typed-contract", "title": "型付き契約", "summary": "同じ識別子と文書構造を二言語で検証します。"},
|
|
20
|
+
{"id": "seo-ready", "title": "SEO対応", "summary": "lang、canonical、hreflangをURL戦略から生成します。"}
|
|
21
|
+
],
|
|
22
|
+
"docs": {
|
|
23
|
+
"overview": {
|
|
24
|
+
"slug": "/docs",
|
|
25
|
+
"title": "概要",
|
|
26
|
+
"summary": "サービスの構成と責務を説明します。",
|
|
27
|
+
"sections": [
|
|
28
|
+
{"id": "purpose", "title": "目的", "body": ["公開情報を小さな契約として安全に提供します。"]}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"gettingStarted": {
|
|
32
|
+
"slug": "/docs/getting-started",
|
|
33
|
+
"title": "はじめに",
|
|
34
|
+
"summary": "最小構成でサイトをビルドする手順です。",
|
|
35
|
+
"sections": [
|
|
36
|
+
{"id": "install", "title": "導入", "body": ["公開確認済みのパッケージを導入し、設定ファイルを配置します。"]}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"security": {
|
|
40
|
+
"slug": "/docs/security",
|
|
41
|
+
"title": "セキュリティ",
|
|
42
|
+
"summary": "情報境界と外部操作の方針です。",
|
|
43
|
+
"sections": [
|
|
44
|
+
{"id": "boundary", "title": "境界", "body": ["任意のメンテナンス案内は同一originから読み取ります。外部操作は実装しません。"]}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"privacy": {"label": "プライバシー", "summary": "公開承認済みの情報のみを表示します。"},
|
|
49
|
+
"contact": {"label": "お問い合わせ", "summary": "連絡方法はサービス運営者が別途定めます。"}
|
|
50
|
+
},
|
|
51
|
+
"en": {
|
|
52
|
+
"brand": "Sample Service",
|
|
53
|
+
"tagline": "A public site built from a local contract",
|
|
54
|
+
"summary": "This example maintains Japanese and English in one structure.",
|
|
55
|
+
"features": [
|
|
56
|
+
{"id": "local-first", "title": "Local first", "summary": "Only explicitly selected content is read during the build."},
|
|
57
|
+
{"id": "typed-contract", "title": "Typed contract", "summary": "Both locales share validated identifiers and document structure."},
|
|
58
|
+
{"id": "seo-ready", "title": "SEO ready", "summary": "URL policy generates lang, canonical, and hreflang metadata."}
|
|
59
|
+
],
|
|
60
|
+
"docs": {
|
|
61
|
+
"overview": {
|
|
62
|
+
"slug": "/docs",
|
|
63
|
+
"title": "Overview",
|
|
64
|
+
"summary": "Understand the service structure and responsibility.",
|
|
65
|
+
"sections": [
|
|
66
|
+
{"id": "purpose", "title": "Purpose", "body": ["Publish approved information through a small contract."]}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"gettingStarted": {
|
|
70
|
+
"slug": "/docs/getting-started",
|
|
71
|
+
"title": "Getting started",
|
|
72
|
+
"summary": "Build the site from the minimum consumer files.",
|
|
73
|
+
"sections": [
|
|
74
|
+
{"id": "install", "title": "Install", "body": ["Install the verified published package and provide the site configuration."]}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"security": {
|
|
78
|
+
"slug": "/docs/security",
|
|
79
|
+
"title": "Security",
|
|
80
|
+
"summary": "Review information boundaries and external-action policy.",
|
|
81
|
+
"sections": [
|
|
82
|
+
{"id": "boundary", "title": "Boundary", "body": ["The optional maintenance notice reads same-origin data. The module implements no external actions."]}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"privacy": {"label": "Privacy", "summary": "Only information approved for publication is rendered."},
|
|
87
|
+
"contact": {"label": "Contact", "summary": "The service operator defines the contact channel separately."}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|