diff options
-rw-r--r-- | client/src/app/app.module.ts | 18 | ||||
-rw-r--r-- | client/src/app/core/server/server.service.ts | 20 | ||||
-rw-r--r-- | client/src/app/shared/i18n/i18n-utils.ts | 12 | ||||
-rw-r--r-- | client/src/app/shared/video/video.service.ts | 4 | ||||
-rw-r--r-- | client/src/app/videos/+video-watch/video-watch.component.ts | 3 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player.ts | 10 | ||||
-rw-r--r-- | client/src/main.ts | 7 | ||||
-rwxr-xr-x | scripts/i18n/xliff2json.ts | 4 | ||||
-rw-r--r-- | server/controllers/client.ts | 22 | ||||
-rw-r--r-- | shared/models/i18n/i18n.ts | 35 |
10 files changed, 81 insertions, 54 deletions
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index e60a74cc0..51e354378 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -16,8 +16,8 @@ import { MenuComponent } from './menu' | |||
16 | import { SharedModule } from './shared' | 16 | import { SharedModule } from './shared' |
17 | import { SignupModule } from './signup' | 17 | import { SignupModule } from './signup' |
18 | import { VideosModule } from './videos' | 18 | import { VideosModule } from './videos' |
19 | import { buildFileLocale, getDefaultLocale } from '../../../shared/models/i18n' | 19 | import { buildFileLocale, getCompleteLocale, getDefaultLocale, isDefaultLocale } from '../../../shared/models/i18n' |
20 | import { environment } from '../environments/environment' | 20 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
21 | 21 | ||
22 | export function metaFactory (serverService: ServerService): MetaLoader { | 22 | export function metaFactory (serverService: ServerService): MetaLoader { |
23 | return new MetaStaticLoader({ | 23 | return new MetaStaticLoader({ |
@@ -67,17 +67,17 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
67 | { | 67 | { |
68 | provide: TRANSLATIONS, | 68 | provide: TRANSLATIONS, |
69 | useFactory: (locale) => { | 69 | useFactory: (locale) => { |
70 | // On dev mode, test locales | 70 | // On dev mode, test localization |
71 | if (environment.production === false && window.location.search === '?lang=fr') { | 71 | if (isOnDevLocale()) { |
72 | return require(`raw-loader!../locale/target/angular_fr.xml`) | 72 | locale = getDevLocale() |
73 | return require(`raw-loader!../locale/target/angular_${locale}.xml`) | ||
73 | } | 74 | } |
74 | 75 | ||
75 | const fileLocale = buildFileLocale(locale) | ||
76 | |||
77 | // Default locale, nothing to translate | 76 | // Default locale, nothing to translate |
78 | const defaultFileLocale = buildFileLocale(getDefaultLocale()) | 77 | const completeLocale = getCompleteLocale(locale) |
79 | if (fileLocale === defaultFileLocale) return '' | 78 | if (isDefaultLocale(completeLocale)) return '' |
80 | 79 | ||
80 | const fileLocale = buildFileLocale(locale) | ||
81 | return require(`raw-loader!../locale/target/angular_${fileLocale}.xml`) | 81 | return require(`raw-loader!../locale/target/angular_${fileLocale}.xml`) |
82 | }, | 82 | }, |
83 | deps: [ LOCALE_ID ] | 83 | deps: [ LOCALE_ID ] |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 56d33339e..74363e6a1 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -2,13 +2,13 @@ import { map, share, switchMap, tap } from 'rxjs/operators' | |||
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient } from '@angular/common/http' |
3 | import { Inject, Injectable, LOCALE_ID } from '@angular/core' | 3 | import { Inject, Injectable, LOCALE_ID } from '@angular/core' |
4 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 4 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
5 | import { Observable, ReplaySubject } from 'rxjs' | 5 | import { Observable, ReplaySubject, of } from 'rxjs' |
6 | import { ServerConfig } from '../../../../../shared' | 6 | import { getCompleteLocale, ServerConfig } from '../../../../../shared' |
7 | import { About } from '../../../../../shared/models/server/about.model' | 7 | import { About } from '../../../../../shared/models/server/about.model' |
8 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
9 | import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' | 9 | import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' |
10 | import { buildFileLocale, getDefaultLocale } from '../../../../../shared/models/i18n' | 10 | import { isDefaultLocale } from '../../../../../shared/models/i18n' |
11 | import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' | 11 | import { getDevLocale, isOnDevLocale, peertubeTranslate } from '@app/shared/i18n/i18n-utils' |
12 | 12 | ||
13 | @Injectable() | 13 | @Injectable() |
14 | export class ServerService { | 14 | export class ServerService { |
@@ -72,8 +72,8 @@ export class ServerService { | |||
72 | private http: HttpClient, | 72 | private http: HttpClient, |
73 | @Inject(LOCALE_ID) private localeId: string | 73 | @Inject(LOCALE_ID) private localeId: string |
74 | ) { | 74 | ) { |
75 | this.loadConfigLocally() | ||
76 | this.loadServerLocale() | 75 | this.loadServerLocale() |
76 | this.loadConfigLocally() | ||
77 | } | 77 | } |
78 | 78 | ||
79 | loadConfig () { | 79 | loadConfig () { |
@@ -163,14 +163,16 @@ export class ServerService { | |||
163 | } | 163 | } |
164 | 164 | ||
165 | private loadServerLocale () { | 165 | private loadServerLocale () { |
166 | const fileLocale = buildFileLocale(environment.production === true ? this.localeId : 'fr') | 166 | const completeLocale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId) |
167 | 167 | ||
168 | // Default locale, nothing to translate | 168 | // Default locale, nothing to translate |
169 | const defaultFileLocale = buildFileLocale(getDefaultLocale()) | 169 | if (isDefaultLocale(completeLocale)) { |
170 | if (fileLocale === defaultFileLocale) return {} | 170 | this.localeObservable = of({}).pipe(share()) |
171 | return | ||
172 | } | ||
171 | 173 | ||
172 | this.localeObservable = this.http | 174 | this.localeObservable = this.http |
173 | .get(ServerService.BASE_LOCALE_URL + fileLocale + '/server.json') | 175 | .get(ServerService.BASE_LOCALE_URL + completeLocale + '/server.json') |
174 | .pipe(share()) | 176 | .pipe(share()) |
175 | } | 177 | } |
176 | 178 | ||
diff --git a/client/src/app/shared/i18n/i18n-utils.ts b/client/src/app/shared/i18n/i18n-utils.ts index c1de51b7b..37180b930 100644 --- a/client/src/app/shared/i18n/i18n-utils.ts +++ b/client/src/app/shared/i18n/i18n-utils.ts | |||
@@ -1,7 +1,19 @@ | |||
1 | import { environment } from '../../../environments/environment' | ||
2 | |||
1 | function peertubeTranslate (str: string, translations: { [ id: string ]: string }) { | 3 | function peertubeTranslate (str: string, translations: { [ id: string ]: string }) { |
2 | return translations[str] ? translations[str] : str | 4 | return translations[str] ? translations[str] : str |
3 | } | 5 | } |
4 | 6 | ||
7 | function isOnDevLocale () { | ||
8 | return environment.production === false && window.location.search === '?lang=fr' | ||
9 | } | ||
10 | |||
11 | function getDevLocale () { | ||
12 | return 'fr' | ||
13 | } | ||
14 | |||
5 | export { | 15 | export { |
16 | getDevLocale, | ||
17 | isOnDevLocale, | ||
6 | peertubeTranslate | 18 | peertubeTranslate |
7 | } | 19 | } |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index c607b7d6a..58cb52efc 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -46,8 +46,8 @@ export class VideoService { | |||
46 | return this.serverService.localeObservable | 46 | return this.serverService.localeObservable |
47 | .pipe( | 47 | .pipe( |
48 | switchMap(translations => { | 48 | switchMap(translations => { |
49 | return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid) | 49 | return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid) |
50 | .pipe(map(videoHash => ({ videoHash, translations }))) | 50 | .pipe(map(videoHash => ({ videoHash, translations }))) |
51 | }), | 51 | }), |
52 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), | 52 | map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), |
53 | catchError(res => this.restExtractor.handleError(res)) | 53 | catchError(res => this.restExtractor.handleError(res)) |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index d3e16c4cf..4a67d456e 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -25,6 +25,7 @@ import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/p | |||
25 | import { ServerService } from '@app/core' | 25 | import { ServerService } from '@app/core' |
26 | import { I18n } from '@ngx-translate/i18n-polyfill' | 26 | import { I18n } from '@ngx-translate/i18n-polyfill' |
27 | import { environment } from '../../../environments/environment' | 27 | import { environment } from '../../../environments/environment' |
28 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
28 | 29 | ||
29 | @Component({ | 30 | @Component({ |
30 | selector: 'my-video-watch', | 31 | selector: 'my-video-watch', |
@@ -377,7 +378,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
377 | }) | 378 | }) |
378 | 379 | ||
379 | if (this.videojsLocaleLoaded === false) { | 380 | if (this.videojsLocaleLoaded === false) { |
380 | await loadLocale(environment.apiUrl, videojs, environment.production === true ? this.localeId : 'fr') | 381 | await loadLocale(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) |
381 | this.videojsLocaleLoaded = true | 382 | this.videojsLocaleLoaded = true |
382 | } | 383 | } |
383 | 384 | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts index b604097fa..9e37b75d2 100644 --- a/client/src/assets/player/peertube-player.ts +++ b/client/src/assets/player/peertube-player.ts | |||
@@ -12,7 +12,7 @@ import './peertube-videojs-plugin' | |||
12 | import './peertube-load-progress-bar' | 12 | import './peertube-load-progress-bar' |
13 | import { videojsUntyped } from './peertube-videojs-typings' | 13 | import { videojsUntyped } from './peertube-videojs-typings' |
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | 14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' |
15 | import { is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | 15 | import { getCompleteLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' |
16 | 16 | ||
17 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 17 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
18 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | 18 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' |
@@ -141,11 +141,13 @@ function addContextMenu (player: any, videoEmbedUrl: string) { | |||
141 | } | 141 | } |
142 | 142 | ||
143 | function loadLocale (serverUrl: string, videojs: any, locale: string) { | 143 | function loadLocale (serverUrl: string, videojs: any, locale: string) { |
144 | if (!is18nLocale(locale) || isDefaultLocale(locale)) return undefined | 144 | const completeLocale = getCompleteLocale(locale) |
145 | 145 | ||
146 | return fetch(serverUrl + '/client/locales/' + locale + '/player.json') | 146 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return Promise.resolve(undefined) |
147 | |||
148 | return fetch(serverUrl + '/client/locales/' + completeLocale + '/player.json') | ||
147 | .then(res => res.json()) | 149 | .then(res => res.json()) |
148 | .then(json => videojs.addLanguage(locale, json)) | 150 | .then(json => videojs.addLanguage(completeLocale, json)) |
149 | } | 151 | } |
150 | 152 | ||
151 | export { | 153 | export { |
diff --git a/client/src/main.ts b/client/src/main.ts index 19f45a3e3..061be17de 100644 --- a/client/src/main.ts +++ b/client/src/main.ts | |||
@@ -5,14 +5,17 @@ import { AppModule } from './app/app.module' | |||
5 | import { environment } from './environments/environment' | 5 | import { environment } from './environments/environment' |
6 | 6 | ||
7 | import { hmrBootstrap } from './hmr' | 7 | import { hmrBootstrap } from './hmr' |
8 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
8 | 9 | ||
9 | let providers = [] | 10 | let providers = [] |
10 | if (environment.production) { | 11 | if (environment.production) { |
11 | enableProdMode() | 12 | enableProdMode() |
12 | } | 13 | } |
13 | 14 | ||
14 | if (environment.production === false && window.location.search === '?lang=fr') { | 15 | // Template translation, should be in the bootstrap step |
15 | const translations = require(`raw-loader!./locale/target/angular_fr.xml`) | 16 | if (isOnDevLocale()) { |
17 | const locale = getDevLocale() | ||
18 | const translations = require(`raw-loader!./locale/target/angular_${locale}.xml`) | ||
16 | 19 | ||
17 | providers = [ | 20 | providers = [ |
18 | { provide: TRANSLATIONS, useValue: translations }, | 21 | { provide: TRANSLATIONS, useValue: translations }, |
diff --git a/scripts/i18n/xliff2json.ts b/scripts/i18n/xliff2json.ts index fa5a71d65..c60739561 100755 --- a/scripts/i18n/xliff2json.ts +++ b/scripts/i18n/xliff2json.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as xliff12ToJs from 'xliff/xliff12ToJs' | 1 | import * as xliff12ToJs from 'xliff/xliff12ToJs' |
2 | import { unlink, readFileSync, writeFile } from 'fs' | 2 | import { unlink, readFileSync, writeFile } from 'fs' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { buildFileLocale, I18N_LOCALES, isDefaultLocale } from '../../shared/models/i18n/i18n' | 4 | import { buildFileLocale, I18N_LOCALES, isDefaultLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' |
5 | import { eachSeries } from 'async' | 5 | import { eachSeries } from 'async' |
6 | 6 | ||
7 | const sources: string[] = [] | 7 | const sources: string[] = [] |
@@ -9,7 +9,7 @@ const availableLocales = Object.keys(I18N_LOCALES) | |||
9 | .filter(l => isDefaultLocale(l) === false) | 9 | .filter(l => isDefaultLocale(l) === false) |
10 | .map(l => buildFileLocale(l)) | 10 | .map(l => buildFileLocale(l)) |
11 | 11 | ||
12 | for (const file of [ 'server', 'player' ]) { | 12 | for (const file of LOCALE_FILES) { |
13 | for (const locale of availableLocales) { | 13 | for (const locale of availableLocales) { |
14 | sources.push(join(__dirname, '../../../client/src/locale/target/', `${file}_${locale}.xml`)) | 14 | sources.push(join(__dirname, '../../../client/src/locale/target/', `${file}_${locale}.xml`)) |
15 | } | 15 | } |
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index ec78a4bbc..385757fa6 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -3,18 +3,12 @@ import * as express from 'express' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import * as validator from 'validator' | 4 | import * as validator from 'validator' |
5 | import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' | 5 | import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' |
6 | import { | 6 | import { ACCEPT_HEADERS, CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' |
7 | ACCEPT_HEADERS, | ||
8 | CONFIG, | ||
9 | EMBED_SIZE, | ||
10 | OPENGRAPH_AND_OEMBED_COMMENT, | ||
11 | STATIC_MAX_AGE, | ||
12 | STATIC_PATHS | ||
13 | } from '../initializers' | ||
14 | import { asyncMiddleware } from '../middlewares' | 7 | import { asyncMiddleware } from '../middlewares' |
15 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
16 | import { VideoPrivacy } from '../../shared/models/videos' | 9 | import { VideoPrivacy } from '../../shared/models/videos' |
17 | import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models' | 10 | import { buildFileLocale, getCompleteLocale, getDefaultLocale, is18nLocale } from '../../shared/models' |
11 | import { LOCALE_FILES } from '../../shared/models/i18n/i18n' | ||
18 | 12 | ||
19 | const clientsRouter = express.Router() | 13 | const clientsRouter = express.Router() |
20 | 14 | ||
@@ -51,8 +45,10 @@ clientsRouter.use('/client/locales/:locale/:file.json', function (req, res) { | |||
51 | const locale = req.params.locale | 45 | const locale = req.params.locale |
52 | const file = req.params.file | 46 | const file = req.params.file |
53 | 47 | ||
54 | if (is18nLocale(locale) && [ 'player', 'server' ].indexOf(file) !== -1) { | 48 | if (is18nLocale(locale) && LOCALE_FILES.indexOf(file) !== -1) { |
55 | return res.sendFile(join(__dirname, `../../../client/dist/locale/${file}_${locale}.json`)) | 49 | const completeLocale = getCompleteLocale(locale) |
50 | const completeFileLocale = buildFileLocale(completeLocale) | ||
51 | return res.sendFile(join(__dirname, `../../../client/dist/locale/${file}_${completeFileLocale}.json`)) | ||
56 | } | 52 | } |
57 | 53 | ||
58 | return res.sendStatus(404) | 54 | return res.sendStatus(404) |
@@ -88,12 +84,12 @@ function getIndexPath (req: express.Request, paramLang?: string) { | |||
88 | if (paramLang && is18nLocale(paramLang)) { | 84 | if (paramLang && is18nLocale(paramLang)) { |
89 | lang = paramLang | 85 | lang = paramLang |
90 | } else { | 86 | } else { |
91 | // lang = req.acceptsLanguages(Object.keys(I18N_LOCALES)) || getDefaultLocale() | 87 | // lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale() |
92 | // Disable auto language for now | 88 | // Disable auto language for now |
93 | lang = getDefaultLocale() | 89 | lang = getDefaultLocale() |
94 | } | 90 | } |
95 | 91 | ||
96 | return join(__dirname, '../../../client/dist/' + lang + '/index.html') | 92 | return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html') |
97 | } | 93 | } |
98 | 94 | ||
99 | function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 95 | function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { |
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts index 4d50bc36e..be1420150 100644 --- a/shared/models/i18n/i18n.ts +++ b/shared/models/i18n/i18n.ts | |||
@@ -1,34 +1,45 @@ | |||
1 | export const LOCALE_FILES = [ 'player', 'server' ] | ||
2 | |||
1 | export const I18N_LOCALES = { | 3 | export const I18N_LOCALES = { |
2 | 'en-US': 'English (US)', | 4 | 'en-US': 'English (US)', |
3 | fr: 'French' | 5 | fr: 'French' |
4 | } | 6 | } |
5 | 7 | ||
8 | const I18N_LOCALE_ALIAS = { | ||
9 | 'en': 'en-US' | ||
10 | } | ||
11 | |||
12 | export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) | ||
13 | .concat(Object.keys(I18N_LOCALE_ALIAS)) | ||
14 | |||
15 | const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l) | ||
16 | |||
6 | export function getDefaultLocale () { | 17 | export function getDefaultLocale () { |
7 | return 'en-US' | 18 | return 'en-US' |
8 | } | 19 | } |
9 | 20 | ||
10 | export function isDefaultLocale (locale: string) { | 21 | export function isDefaultLocale (locale: string) { |
11 | return locale === getDefaultLocale() | 22 | return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale()) |
12 | } | 23 | } |
13 | 24 | ||
14 | const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l) | ||
15 | export function is18nPath (path: string) { | 25 | export function is18nPath (path: string) { |
16 | return possiblePaths.indexOf(path) !== -1 | 26 | return possiblePaths.indexOf(path) !== -1 |
17 | } | 27 | } |
18 | 28 | ||
19 | const possibleLanguages = Object.keys(I18N_LOCALES) | ||
20 | export function is18nLocale (locale: string) { | 29 | export function is18nLocale (locale: string) { |
21 | return possibleLanguages.indexOf(locale) !== -1 | 30 | return POSSIBLE_LOCALES.indexOf(locale) !== -1 |
31 | } | ||
32 | |||
33 | export function getCompleteLocale (locale: string) { | ||
34 | if (!locale) return locale | ||
35 | |||
36 | if (I18N_LOCALE_ALIAS[locale]) return I18N_LOCALE_ALIAS[locale] | ||
37 | |||
38 | return locale | ||
22 | } | 39 | } |
23 | 40 | ||
24 | // Only use in dev mode, so relax | ||
25 | // In production, the locale always match with a I18N_LANGUAGES key | ||
26 | export function buildFileLocale (locale: string) { | 41 | export function buildFileLocale (locale: string) { |
27 | if (!is18nLocale(locale)) { | 42 | const completeLocale = getCompleteLocale(locale) |
28 | // Some working examples for development purpose | ||
29 | if (locale.split('-')[ 0 ] === 'en') return 'en_US' | ||
30 | else if (locale === 'fr') return 'fr' | ||
31 | } | ||
32 | 43 | ||
33 | return locale.replace('-', '_') | 44 | return completeLocale.replace('-', '_') |
34 | } | 45 | } |