wip
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"globals": {
|
||||
"_DEV_": false,
|
||||
"_LANG_": false,
|
||||
"_LANGS_": false,
|
||||
"_LOCALE_": false,
|
||||
"_VERSION_": false,
|
||||
"_ENV_": false,
|
||||
"_PERF_PREFIX_": false,
|
||||
|
4
src/client/@types/global.d.ts
vendored
4
src/client/@types/global.d.ts
vendored
@@ -1,4 +1,6 @@
|
||||
declare const _LANGS_: string[];
|
||||
declare const _LANG_: string;
|
||||
declare const _LANGS_: string[][];
|
||||
declare const _LOCALE_: Record<string, any>;
|
||||
declare const _VERSION_: string;
|
||||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
|
@@ -2,11 +2,11 @@
|
||||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
|
||||
<template #header>
|
||||
<Fa :icon="faExclamationCircle" style="margin-right: 0.5em;"/>
|
||||
<i18n-t keypath="reportAbuseOf" tag="span">
|
||||
<I18n src="reportAbuseOf" tag="span">
|
||||
<template #name>
|
||||
<b><MkAcct :user="user"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</template>
|
||||
<div class="dpvffvvy">
|
||||
<div class="_section">
|
||||
|
@@ -6,19 +6,19 @@
|
||||
<div class="status">
|
||||
<div>
|
||||
<Fa :icon="faUsers" fixed-width/>
|
||||
<i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<I18n src="_channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.usersCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
<div>
|
||||
<Fa :icon="faPencilAlt" fixed-width/>
|
||||
<i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<I18n src="_channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.notesCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
15
src/client/components/global/i18n.ts
Normal file
15
src/client/components/global/i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { h, Fragment, defineComponent } from 'vue';
|
||||
import type { SetupContext, VNodeChild, RenderFunction } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
render() {
|
||||
// TODO
|
||||
return h('span', this.src);
|
||||
}
|
||||
});
|
@@ -9,6 +9,7 @@ import userName from './global/user-name.vue';
|
||||
import ellipsis from './global/ellipsis.vue';
|
||||
import time from './global/time.vue';
|
||||
import url from './global/url.vue';
|
||||
import i18n from './global/i18n';
|
||||
import loading from './global/loading.vue';
|
||||
import error from './global/error.vue';
|
||||
|
||||
@@ -24,4 +25,5 @@ export default function(app: App) {
|
||||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('I18n', i18n);
|
||||
}
|
||||
|
@@ -16,13 +16,13 @@
|
||||
<div class="renote" v-if="isRenote">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<Fa :icon="faRetweet"/>
|
||||
<i18n-t keypath="renotedBy" tag="span">
|
||||
<I18n src="renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div class="info">
|
||||
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
|
||||
<Fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
|
||||
@@ -90,13 +90,13 @@
|
||||
<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<i18n-t keypath="userSaysSomething" tag="small">
|
||||
<I18n src="userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -38,9 +38,9 @@
|
||||
</MkInput>
|
||||
<label v-if="meta.tosUrl" class="tou">
|
||||
<input type="checkbox" v-model="ToSAgreement">
|
||||
<i18n-t keypath="agreeTo">
|
||||
<I18n src="agreeTo">
|
||||
<a :href="meta.tosUrl" class="_link" target="_blank">{{ $t('tos') }}</a>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</label>
|
||||
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
|
||||
<captcha v-if="meta.enableRecaptcha" class="captcha" provider="grecaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { clientDb, entries } from './db';
|
||||
|
||||
const address = new URL(location.href);
|
||||
const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
|
||||
|
||||
@@ -8,9 +6,9 @@ export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const lang = localStorage.getItem('lang');
|
||||
export const lang = _LANG_;
|
||||
export const langs = _LANGS_;
|
||||
export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]);
|
||||
export const locale = _LOCALE_;
|
||||
export const version = _VERSION_;
|
||||
export const instanceName = siteName === 'Misskey' ? host : siteName;
|
||||
export const ui = localStorage.getItem('ui');
|
||||
|
@@ -1,36 +1,49 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { clientDb, get, count } from './db';
|
||||
import { setI18nContexts } from '@/scripts/set-i18n-contexts';
|
||||
import { version, langs, getLocale } from '@/config';
|
||||
import { markRaw } from 'vue';
|
||||
import { locale } from '@/config';
|
||||
|
||||
let _lang = localStorage.getItem('lang');
|
||||
export class I18n<T extends Record<string, any>> {
|
||||
public locale: T;
|
||||
|
||||
if (_lang == null) {
|
||||
if (langs.map(x => x[0]).includes(navigator.language)) {
|
||||
_lang = navigator.language;
|
||||
} else {
|
||||
_lang = langs.map(x => x[0]).find(x => x.split('-')[0] == navigator.language);
|
||||
constructor(locale: T) {
|
||||
this.locale = locale;
|
||||
|
||||
if (_lang == null) {
|
||||
// Fallback
|
||||
_lang = 'en-US';
|
||||
if (_DEV_) {
|
||||
console.log('i18n', this.locale);
|
||||
}
|
||||
|
||||
//#region BIND
|
||||
this.t = this.t.bind(this);
|
||||
//#endregion
|
||||
}
|
||||
|
||||
localStorage.setItem('lang', _lang);
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, any>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
} catch (e) {
|
||||
if (_DEV_) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return `⚠'${key}'⚠`;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const lang = _lang;
|
||||
export const i18n = markRaw(new I18n(locale));
|
||||
|
||||
export const locale = await count(clientDb.i18n).then(async n => {
|
||||
if (n === 0) return await setI18nContexts(_lang, version);
|
||||
if ((await get('_version_', clientDb.i18n) !== version)) return await setI18nContexts(_lang, version, true);
|
||||
|
||||
return await getLocale();
|
||||
});
|
||||
|
||||
export const i18n = createI18n({
|
||||
sync: false,
|
||||
locale: _lang,
|
||||
messages: { [_lang]: locale }
|
||||
});
|
||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: typeof i18n['t'];
|
||||
$ts: typeof i18n['locale'];
|
||||
}
|
||||
}
|
||||
|
@@ -40,11 +40,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
import components from '@/components';
|
||||
import { version, ui } from '@/config';
|
||||
import { version, ui, lang } from '@/config';
|
||||
import { router } from '@/router';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { i18n, lang } from '@/i18n';
|
||||
import { i18n } from '@/i18n';
|
||||
import { stream, isMobile, dialog } from '@/os';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||
@@ -53,6 +53,8 @@ import { fetchInstance, instance } from '@/instance';
|
||||
|
||||
console.info(`Misskey v${version}`);
|
||||
|
||||
window.clearTimeout(window.mkBootTimer);
|
||||
|
||||
if (_DEV_) {
|
||||
console.warn('Development mode!!!');
|
||||
|
||||
@@ -175,10 +177,11 @@ app.config.globalProperties = {
|
||||
$i,
|
||||
$store: defaultStore,
|
||||
$instance: instance,
|
||||
$t: i18n.t,
|
||||
$ts: i18n.locale,
|
||||
};
|
||||
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
// eslint-disable-next-line vue/component-definition-name-casing
|
||||
app.component('Fa', FontAwesomeIcon);
|
||||
|
||||
|
@@ -10,8 +10,8 @@
|
||||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><Fa :icon="faUsers" fixed-width/><i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></i18n-t></div>
|
||||
<div><Fa :icon="faPencilAlt" fixed-width/><i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></i18n-t></div>
|
||||
<div><Fa :icon="faUsers" fixed-width/><I18n src="_channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><Fa :icon="faPencilAlt" fixed-width/><I18n src="_channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
|
@@ -35,18 +35,18 @@
|
||||
<div>
|
||||
<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $t('random') }}</MkRadio>
|
||||
<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
|
||||
<i18n-t keypath="_reversi.blackIs" tag="span">
|
||||
<I18n src="_reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user1"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</MkRadio>
|
||||
<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
|
||||
<i18n-t keypath="_reversi.blackIs" tag="span">
|
||||
<I18n src="_reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user2"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</MkRadio>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -46,11 +46,11 @@
|
||||
</div>
|
||||
<div class="sazhgisb" v-else>
|
||||
<h1>
|
||||
<i18n-t keypath="waitingFor" tag="span">
|
||||
<I18n src="waitingFor" tag="span">
|
||||
<template #x>
|
||||
<b><MkUserName :user="matching"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</h1>
|
||||
<div class="cancel">
|
||||
|
@@ -45,14 +45,14 @@
|
||||
<div v-if="data && !$i.twoFactorEnabled">
|
||||
<ol style="margin: 0; padding: 0 0 0 1em;">
|
||||
<li>
|
||||
<i18n-t keypath="_2fa.step1" tag="span">
|
||||
<I18n src="_2fa.step1" tag="span">
|
||||
<template #a>
|
||||
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
|
||||
</template>
|
||||
<template #b>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</li>
|
||||
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
|
||||
<li>{{ $t('_2fa.step3') }}<br>
|
||||
|
@@ -6,11 +6,11 @@
|
||||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
<template #caption>
|
||||
<i18n-t keypath="i18nInfo" tag="span">
|
||||
<I18n src="i18nInfo" tag="span">
|
||||
<template #link>
|
||||
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</template>
|
||||
</FormSelect>
|
||||
|
||||
|
@@ -23,14 +23,14 @@
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 4">
|
||||
<div>{{ $t('_tutorial.step5_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step5_2" tag="div">
|
||||
<I18n src="_tutorial.step5_2" tag="div">
|
||||
<template #featured>
|
||||
<MkA class="_link" to="/featured">{{ $t('featured') }}</MkA>
|
||||
</template>
|
||||
<template #explore>
|
||||
<MkA class="_link" to="/explore">{{ $t('explore') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div>{{ $t('_tutorial.step5_3') }}</div>
|
||||
<small>{{ $t('_tutorial.step5_4') }}</small>
|
||||
</div>
|
||||
@@ -41,11 +41,11 @@
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 6">
|
||||
<div>{{ $t('_tutorial.step7_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step7_2" tag="div">
|
||||
<I18n src="_tutorial.step7_2" tag="div">
|
||||
<template #help>
|
||||
<MkA class="_link" to="/docs">{{ $t('help') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div>{{ $t('_tutorial.step7_3') }}</div>
|
||||
</div>
|
||||
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { clientDb, clear, bulkSet } from '../db';
|
||||
import { deepEntries, delimitEntry } from 'deep-entries';
|
||||
|
||||
export function setI18nContexts(lang: string, version: string, cleardb = false) {
|
||||
return Promise.all([
|
||||
cleardb ? clear(clientDb.i18n) : Promise.resolve(),
|
||||
fetch(`/assets/locales/${lang}.${version}.json`)
|
||||
])
|
||||
.then(([, response]) => response.json())
|
||||
.then(locale => {
|
||||
const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][];
|
||||
bulkSet(flatLocaleEntries, clientDb.i18n);
|
||||
return Object.fromEntries(flatLocaleEntries);
|
||||
});
|
||||
}
|
@@ -177,7 +177,7 @@ export default defineComponent({
|
||||
// TODO: この値を設定で変えられるようにする?
|
||||
$columnMargin: 12px;
|
||||
|
||||
$deckMargin: 12px;
|
||||
$deckMargin: $columnMargin;
|
||||
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
|
Reference in New Issue
Block a user